@voodocs/cli 0.1.1 → 0.2.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 +98 -0
- package/README.md +37 -3
- package/USAGE.md +52 -2
- package/cli.py +262 -2
- package/examples/test_function_invariants.py +134 -0
- package/lib/darkarts/annotations/parser.py +64 -0
- package/lib/darkarts/context/__init__.py +84 -0
- package/lib/darkarts/context/checker.py +379 -0
- package/lib/darkarts/context/commands.py +1688 -0
- package/lib/darkarts/context/diagram.py +300 -0
- package/lib/darkarts/context/models.py +200 -0
- package/lib/darkarts/context/yaml_utils.py +342 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +1 -1
- package/lib/darkarts/telemetry.py +1 -1
- package/package.json +2 -1
- package/templates/CONTEXT_TEMPLATE.md +152 -0
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context System Commands
|
|
3
|
+
|
|
4
|
+
Implements the command-line interface for the context system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from .models import create_minimal_context
|
|
12
|
+
from .yaml_utils import (
|
|
13
|
+
write_context_yaml,
|
|
14
|
+
read_context_yaml,
|
|
15
|
+
add_to_gitignore,
|
|
16
|
+
format_context_as_markdown
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_project_root() -> Path:
|
|
21
|
+
"""
|
|
22
|
+
Get the project root directory (current working directory).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Path to the project root
|
|
26
|
+
"""
|
|
27
|
+
return Path.cwd()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_context_file_path() -> Path:
|
|
31
|
+
"""
|
|
32
|
+
Get the path to the .voodocs.context file.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Path to .voodocs.context in the project root
|
|
36
|
+
"""
|
|
37
|
+
return get_project_root() / '.voodocs.context'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def context_file_exists() -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Check if a .voodocs.context file already exists.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if file exists, False otherwise
|
|
46
|
+
"""
|
|
47
|
+
return get_context_file_path().exists()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def prompt_user(question: str, default: Optional[str] = None, required: bool = True) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Prompt the user for input with an optional default value.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
question: The question to ask
|
|
56
|
+
default: Optional default value
|
|
57
|
+
required: Whether the field is required
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
User's response or default value
|
|
61
|
+
"""
|
|
62
|
+
if default is not None:
|
|
63
|
+
response = input(f"{question} [{default}]: ").strip()
|
|
64
|
+
return response if response else default
|
|
65
|
+
else:
|
|
66
|
+
response = input(f"{question}: ").strip()
|
|
67
|
+
if required:
|
|
68
|
+
while not response:
|
|
69
|
+
print("This field is required.")
|
|
70
|
+
response = input(f"{question}: ").strip()
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def prompt_yes_no(question: str, default: bool = True) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Prompt the user for a yes/no answer.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
question: The question to ask
|
|
80
|
+
default: Default value (True for yes, False for no)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True for yes, False for no
|
|
84
|
+
"""
|
|
85
|
+
default_str = "Y/n" if default else "y/N"
|
|
86
|
+
response = input(f"{question} [{default_str}]: ").strip().lower()
|
|
87
|
+
|
|
88
|
+
if not response:
|
|
89
|
+
return default
|
|
90
|
+
|
|
91
|
+
return response in ['y', 'yes']
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def detect_code_version() -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Try to detect the code version from common files.
|
|
97
|
+
|
|
98
|
+
Checks:
|
|
99
|
+
- package.json (version field)
|
|
100
|
+
- pyproject.toml (version field)
|
|
101
|
+
- setup.py (version argument)
|
|
102
|
+
- VERSION file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Detected version string or None
|
|
106
|
+
"""
|
|
107
|
+
project_root = get_project_root()
|
|
108
|
+
|
|
109
|
+
# Try package.json
|
|
110
|
+
package_json = project_root / 'package.json'
|
|
111
|
+
if package_json.exists():
|
|
112
|
+
try:
|
|
113
|
+
import json
|
|
114
|
+
with open(package_json, 'r') as f:
|
|
115
|
+
data = json.load(f)
|
|
116
|
+
if 'version' in data:
|
|
117
|
+
return data['version']
|
|
118
|
+
except:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Try pyproject.toml
|
|
122
|
+
pyproject_toml = project_root / 'pyproject.toml'
|
|
123
|
+
if pyproject_toml.exists():
|
|
124
|
+
try:
|
|
125
|
+
with open(pyproject_toml, 'r') as f:
|
|
126
|
+
for line in f:
|
|
127
|
+
if line.startswith('version'):
|
|
128
|
+
# Extract version from: version = "1.0.0"
|
|
129
|
+
version = line.split('=')[1].strip().strip('"\'')
|
|
130
|
+
return version
|
|
131
|
+
except:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
# Try VERSION file
|
|
135
|
+
version_file = project_root / 'VERSION'
|
|
136
|
+
if version_file.exists():
|
|
137
|
+
try:
|
|
138
|
+
with open(version_file, 'r') as f:
|
|
139
|
+
return f.read().strip()
|
|
140
|
+
except:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def detect_repository() -> Optional[str]:
|
|
147
|
+
"""
|
|
148
|
+
Try to detect the repository URL from git config.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Repository URL or None
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
import subprocess
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
['git', 'config', '--get', 'remote.origin.url'],
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
cwd=get_project_root()
|
|
160
|
+
)
|
|
161
|
+
if result.returncode == 0:
|
|
162
|
+
url = result.stdout.strip()
|
|
163
|
+
# Convert SSH URL to HTTPS
|
|
164
|
+
if url.startswith('git@github.com:'):
|
|
165
|
+
url = url.replace('git@github.com:', 'https://github.com/')
|
|
166
|
+
if url.endswith('.git'):
|
|
167
|
+
url = url[:-4]
|
|
168
|
+
return url
|
|
169
|
+
except:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cmd_context_init(force: bool = False) -> int:
|
|
176
|
+
"""
|
|
177
|
+
Initialize a new .voodocs.context file for the project.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
force: If True, overwrite existing context file
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Exit code (0 for success, 1 for error)
|
|
184
|
+
"""
|
|
185
|
+
print("🎯 VooDocs Context Initialization")
|
|
186
|
+
print()
|
|
187
|
+
|
|
188
|
+
# Check if context file already exists
|
|
189
|
+
if context_file_exists() and not force:
|
|
190
|
+
print("❌ Error: .voodocs.context already exists in this directory.")
|
|
191
|
+
print(" Use --force to overwrite the existing file.")
|
|
192
|
+
return 1
|
|
193
|
+
|
|
194
|
+
# Detect project information
|
|
195
|
+
detected_version = detect_code_version()
|
|
196
|
+
detected_repo = detect_repository()
|
|
197
|
+
|
|
198
|
+
print("Let's set up your project context. This will create a .voodocs.context file.")
|
|
199
|
+
print()
|
|
200
|
+
|
|
201
|
+
# Prompt for project information
|
|
202
|
+
project_name = prompt_user("Project name")
|
|
203
|
+
|
|
204
|
+
project_purpose = prompt_user("Project purpose (one sentence)")
|
|
205
|
+
|
|
206
|
+
if detected_version:
|
|
207
|
+
code_version = prompt_user("Current code version", detected_version)
|
|
208
|
+
else:
|
|
209
|
+
code_version = prompt_user("Current code version", "1.0.0")
|
|
210
|
+
|
|
211
|
+
# Validate version format
|
|
212
|
+
if not code_version.count('.') >= 2:
|
|
213
|
+
print(f"⚠️ Warning: '{code_version}' doesn't look like semantic versioning (MAJOR.MINOR.PATCH)")
|
|
214
|
+
if not prompt_yes_no("Continue anyway?", default=False):
|
|
215
|
+
print("Aborted.")
|
|
216
|
+
return 1
|
|
217
|
+
|
|
218
|
+
if detected_repo:
|
|
219
|
+
repository = prompt_user("Repository URL (leave empty to skip)", detected_repo, required=False)
|
|
220
|
+
else:
|
|
221
|
+
repository = prompt_user("Repository URL (leave empty to skip)", "", required=False)
|
|
222
|
+
|
|
223
|
+
license_name = prompt_user("License (leave empty to skip)", "", required=False)
|
|
224
|
+
|
|
225
|
+
print()
|
|
226
|
+
print("📝 Configuration Summary:")
|
|
227
|
+
print(f" Project Name: {project_name}")
|
|
228
|
+
print(f" Purpose: {project_purpose}")
|
|
229
|
+
print(f" Code Version: {code_version}")
|
|
230
|
+
if repository:
|
|
231
|
+
print(f" Repository: {repository}")
|
|
232
|
+
if license_name:
|
|
233
|
+
print(f" License: {license_name}")
|
|
234
|
+
print()
|
|
235
|
+
|
|
236
|
+
if not prompt_yes_no("Create context file?", default=True):
|
|
237
|
+
print("Aborted.")
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
# Create context file
|
|
241
|
+
try:
|
|
242
|
+
context = create_minimal_context(
|
|
243
|
+
project_name=project_name,
|
|
244
|
+
project_purpose=project_purpose,
|
|
245
|
+
code_version=code_version,
|
|
246
|
+
repository=repository if repository else None,
|
|
247
|
+
license=license_name if license_name else None
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
context_path = get_context_file_path()
|
|
251
|
+
write_context_yaml(context, context_path)
|
|
252
|
+
|
|
253
|
+
print()
|
|
254
|
+
print(f"✅ Created .voodocs.context")
|
|
255
|
+
print(f" Context version: {context.versioning.context_version}")
|
|
256
|
+
print()
|
|
257
|
+
|
|
258
|
+
# Ask about .gitignore
|
|
259
|
+
if prompt_yes_no("Add .voodocs.context to .gitignore? (recommended for private projects)", default=True):
|
|
260
|
+
if add_to_gitignore(get_project_root()):
|
|
261
|
+
print("✅ Added to .gitignore")
|
|
262
|
+
else:
|
|
263
|
+
print("ℹ️ Already in .gitignore")
|
|
264
|
+
|
|
265
|
+
print()
|
|
266
|
+
print("🎉 Context initialized successfully!")
|
|
267
|
+
print()
|
|
268
|
+
print("Next steps:")
|
|
269
|
+
print(" 1. Review the context file: voodocs context view")
|
|
270
|
+
print(" 2. Add global invariants to the 'invariants.global' section")
|
|
271
|
+
print(" 3. Document architecture decisions in the 'architecture.decisions' section")
|
|
272
|
+
print(" 4. Update context when making changes: voodocs context update")
|
|
273
|
+
print()
|
|
274
|
+
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
print(f"❌ Error creating context file: {e}")
|
|
279
|
+
return 1
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def cmd_context_view(output_file: Optional[str] = None) -> int:
|
|
283
|
+
"""
|
|
284
|
+
View the context file as human-readable Markdown.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
output_file: Optional path to write Markdown output
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Exit code (0 for success, 1 for error)
|
|
291
|
+
"""
|
|
292
|
+
if not context_file_exists():
|
|
293
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
294
|
+
print(" Run 'voodocs context init' to create one.")
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# Read context file
|
|
299
|
+
context_path = get_context_file_path()
|
|
300
|
+
data = read_context_yaml(context_path)
|
|
301
|
+
|
|
302
|
+
if not data:
|
|
303
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
# Parse and format as Markdown
|
|
307
|
+
from .yaml_utils import parse_context_file
|
|
308
|
+
context = parse_context_file(data)
|
|
309
|
+
markdown = format_context_as_markdown(context)
|
|
310
|
+
|
|
311
|
+
if output_file:
|
|
312
|
+
# Write to file
|
|
313
|
+
output_path = Path(output_file)
|
|
314
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
315
|
+
f.write(markdown)
|
|
316
|
+
print(f"✅ Context exported to {output_file}")
|
|
317
|
+
return 0
|
|
318
|
+
else:
|
|
319
|
+
# Print to console
|
|
320
|
+
print(markdown)
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
print(f"❌ Error viewing context: {e}")
|
|
325
|
+
return 1
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def cmd_context_status() -> int:
|
|
329
|
+
"""
|
|
330
|
+
Show the current status of the context file.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Exit code (0 for success, 1 for error)
|
|
334
|
+
"""
|
|
335
|
+
if not context_file_exists():
|
|
336
|
+
print("❌ No context file found")
|
|
337
|
+
print(" Run 'voodocs context init' to create one.")
|
|
338
|
+
return 1
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Read context file
|
|
342
|
+
context_path = get_context_file_path()
|
|
343
|
+
data = read_context_yaml(context_path)
|
|
344
|
+
|
|
345
|
+
if not data:
|
|
346
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
347
|
+
return 1
|
|
348
|
+
|
|
349
|
+
# Extract key information
|
|
350
|
+
versioning = data.get('versioning', {})
|
|
351
|
+
project = data.get('project', {})
|
|
352
|
+
invariants = data.get('invariants', {})
|
|
353
|
+
|
|
354
|
+
print("📊 Context Status")
|
|
355
|
+
print()
|
|
356
|
+
print(f"Project: {project.get('name', 'Unknown')}")
|
|
357
|
+
print(f"Code Version: {versioning.get('code_version', 'Unknown')}")
|
|
358
|
+
print(f"Context Version: {versioning.get('context_version', 'Unknown')}")
|
|
359
|
+
print(f"Last Updated: {versioning.get('last_updated', 'Unknown')}")
|
|
360
|
+
print()
|
|
361
|
+
|
|
362
|
+
# Count sections
|
|
363
|
+
global_invariants = invariants.get('global', [])
|
|
364
|
+
print(f"Global Invariants: {len(global_invariants)}")
|
|
365
|
+
|
|
366
|
+
arch_decisions = data.get('architecture', {}).get('decisions', [])
|
|
367
|
+
print(f"Architecture Decisions: {len(arch_decisions)}")
|
|
368
|
+
|
|
369
|
+
critical_paths = data.get('critical_paths', [])
|
|
370
|
+
print(f"Critical Paths: {len(critical_paths)}")
|
|
371
|
+
|
|
372
|
+
known_issues = data.get('known_issues', [])
|
|
373
|
+
print(f"Known Issues: {len(known_issues)}")
|
|
374
|
+
|
|
375
|
+
print()
|
|
376
|
+
print("Use 'voodocs context view' to see the full context.")
|
|
377
|
+
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
print(f"❌ Error reading context: {e}")
|
|
382
|
+
return 1
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def cmd_context_update(description: Optional[str] = None) -> int:
|
|
386
|
+
"""
|
|
387
|
+
Update the context file and increment the minor version.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
description: Optional description of the changes
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Exit code (0 for success, 1 for error)
|
|
394
|
+
"""
|
|
395
|
+
if not context_file_exists():
|
|
396
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
397
|
+
print(" Run 'voodocs context init' to create one.")
|
|
398
|
+
return 1
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
from datetime import date
|
|
402
|
+
from .models import Change
|
|
403
|
+
|
|
404
|
+
# Read current context
|
|
405
|
+
context_path = get_context_file_path()
|
|
406
|
+
data = read_context_yaml(context_path)
|
|
407
|
+
|
|
408
|
+
if not data:
|
|
409
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
# Get current versions
|
|
413
|
+
versioning = data.get('versioning', {})
|
|
414
|
+
current_context_version = versioning.get('context_version', '0.0')
|
|
415
|
+
code_version = versioning.get('code_version', '0.0.0')
|
|
416
|
+
|
|
417
|
+
# Parse context version
|
|
418
|
+
parts = current_context_version.split('.')
|
|
419
|
+
if len(parts) != 2:
|
|
420
|
+
print(f"❌ Error: Invalid context version format: {current_context_version}")
|
|
421
|
+
return 1
|
|
422
|
+
|
|
423
|
+
major, minor = parts
|
|
424
|
+
new_minor = int(minor) + 1
|
|
425
|
+
new_context_version = f"{major}.{new_minor}"
|
|
426
|
+
|
|
427
|
+
print(f"📝 Updating Context")
|
|
428
|
+
print()
|
|
429
|
+
print(f"Current version: {current_context_version}")
|
|
430
|
+
print(f"New version: {new_context_version}")
|
|
431
|
+
print()
|
|
432
|
+
|
|
433
|
+
# Prompt for description if not provided
|
|
434
|
+
if not description:
|
|
435
|
+
description = prompt_user("Change description", "", required=False)
|
|
436
|
+
if not description:
|
|
437
|
+
description = "Context updated"
|
|
438
|
+
|
|
439
|
+
# Update versioning
|
|
440
|
+
today = date.today().isoformat()
|
|
441
|
+
data['versioning']['context_version'] = new_context_version
|
|
442
|
+
data['versioning']['last_updated'] = today
|
|
443
|
+
|
|
444
|
+
# Add change entry
|
|
445
|
+
if 'changes' not in data:
|
|
446
|
+
data['changes'] = []
|
|
447
|
+
|
|
448
|
+
# Get Git information if available
|
|
449
|
+
commit_hash = get_git_commit_hash()
|
|
450
|
+
author = get_git_author()
|
|
451
|
+
|
|
452
|
+
change_entry = {
|
|
453
|
+
'type': 'context',
|
|
454
|
+
'description': description,
|
|
455
|
+
'date': today,
|
|
456
|
+
'context_version': new_context_version,
|
|
457
|
+
'code_version': code_version
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if commit_hash:
|
|
461
|
+
change_entry['commit'] = commit_hash
|
|
462
|
+
if author:
|
|
463
|
+
change_entry['author'] = author
|
|
464
|
+
|
|
465
|
+
data['changes'].append(change_entry)
|
|
466
|
+
|
|
467
|
+
# Write updated context
|
|
468
|
+
from .yaml_utils import parse_context_file
|
|
469
|
+
context = parse_context_file(data)
|
|
470
|
+
write_context_yaml(context, context_path)
|
|
471
|
+
|
|
472
|
+
print(f"✅ Context updated to version {new_context_version}")
|
|
473
|
+
print(f" Description: {description}")
|
|
474
|
+
print()
|
|
475
|
+
|
|
476
|
+
return 0
|
|
477
|
+
|
|
478
|
+
except Exception as e:
|
|
479
|
+
print(f"❌ Error updating context: {e}")
|
|
480
|
+
import traceback
|
|
481
|
+
traceback.print_exc()
|
|
482
|
+
return 1
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def cmd_context_sync(code_version: Optional[str] = None, force: bool = False) -> int:
|
|
486
|
+
"""
|
|
487
|
+
Sync context version with code version (handles major version bumps).
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
code_version: New code version (e.g., "2.0.0")
|
|
491
|
+
force: Skip confirmation prompt
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Exit code (0 for success, 1 for error)
|
|
495
|
+
"""
|
|
496
|
+
if not context_file_exists():
|
|
497
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
498
|
+
print(" Run 'voodocs context init' to create one.")
|
|
499
|
+
return 1
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
from datetime import date
|
|
503
|
+
|
|
504
|
+
# Read current context
|
|
505
|
+
context_path = get_context_file_path()
|
|
506
|
+
data = read_context_yaml(context_path)
|
|
507
|
+
|
|
508
|
+
if not data:
|
|
509
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
510
|
+
return 1
|
|
511
|
+
|
|
512
|
+
# Get current versions
|
|
513
|
+
versioning = data.get('versioning', {})
|
|
514
|
+
current_code_version = versioning.get('code_version', '0.0.0')
|
|
515
|
+
current_context_version = versioning.get('context_version', '0.0')
|
|
516
|
+
|
|
517
|
+
# If no code version provided, try to detect it
|
|
518
|
+
if not code_version:
|
|
519
|
+
detected_version = detect_code_version()
|
|
520
|
+
if detected_version:
|
|
521
|
+
code_version = prompt_user(
|
|
522
|
+
"New code version",
|
|
523
|
+
detected_version,
|
|
524
|
+
required=True
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
code_version = prompt_user(
|
|
528
|
+
"New code version",
|
|
529
|
+
required=True
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Validate version format
|
|
533
|
+
if code_version.count('.') < 2:
|
|
534
|
+
print(f"❌ Error: Invalid version format: {code_version}")
|
|
535
|
+
print(" Expected: MAJOR.MINOR.PATCH (e.g., 2.0.0)")
|
|
536
|
+
return 1
|
|
537
|
+
|
|
538
|
+
# Extract major versions
|
|
539
|
+
current_major = current_code_version.split('.')[0]
|
|
540
|
+
new_major = code_version.split('.')[0]
|
|
541
|
+
context_major = current_context_version.split('.')[0]
|
|
542
|
+
|
|
543
|
+
print(f"🔄 Syncing Context with Code Version")
|
|
544
|
+
print()
|
|
545
|
+
print(f"Current code version: {current_code_version}")
|
|
546
|
+
print(f"New code version: {code_version}")
|
|
547
|
+
print(f"Current context version: {current_context_version}")
|
|
548
|
+
print()
|
|
549
|
+
|
|
550
|
+
# Check if major version changed
|
|
551
|
+
if current_major != new_major:
|
|
552
|
+
# Major version bump - reset context version
|
|
553
|
+
new_context_version = f"{new_major}.0"
|
|
554
|
+
|
|
555
|
+
print(f"⚠️ MAJOR version change detected ({current_major}.x.x → {new_major}.0.0)")
|
|
556
|
+
print(f" Context version will be reset: {current_context_version} → {new_context_version}")
|
|
557
|
+
print()
|
|
558
|
+
|
|
559
|
+
if not force:
|
|
560
|
+
if not prompt_yes_no("Continue with reset?", default=True):
|
|
561
|
+
print("Aborted.")
|
|
562
|
+
return 1
|
|
563
|
+
|
|
564
|
+
# Update versions
|
|
565
|
+
today = date.today().isoformat()
|
|
566
|
+
data['versioning']['code_version'] = code_version
|
|
567
|
+
data['versioning']['context_version'] = new_context_version
|
|
568
|
+
data['versioning']['last_updated'] = today
|
|
569
|
+
|
|
570
|
+
# Add change entry
|
|
571
|
+
if 'changes' not in data:
|
|
572
|
+
data['changes'] = []
|
|
573
|
+
|
|
574
|
+
# Get Git information if available
|
|
575
|
+
commit_hash = get_git_commit_hash()
|
|
576
|
+
author = get_git_author()
|
|
577
|
+
|
|
578
|
+
change_entry = {
|
|
579
|
+
'type': 'context',
|
|
580
|
+
'description': f"Reset for v{code_version} release",
|
|
581
|
+
'date': today,
|
|
582
|
+
'context_version': new_context_version,
|
|
583
|
+
'code_version': code_version
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if commit_hash:
|
|
587
|
+
change_entry['commit'] = commit_hash
|
|
588
|
+
if author:
|
|
589
|
+
change_entry['author'] = author
|
|
590
|
+
|
|
591
|
+
data['changes'].append(change_entry)
|
|
592
|
+
|
|
593
|
+
# Write updated context
|
|
594
|
+
from .yaml_utils import parse_context_file
|
|
595
|
+
context = parse_context_file(data)
|
|
596
|
+
write_context_yaml(context, context_path)
|
|
597
|
+
|
|
598
|
+
print(f"✅ Context version reset to {new_context_version}")
|
|
599
|
+
print(f" Code version updated to {code_version}")
|
|
600
|
+
print()
|
|
601
|
+
|
|
602
|
+
elif context_major != new_major:
|
|
603
|
+
# Context major doesn't match new code major - this shouldn't happen
|
|
604
|
+
print(f"⚠️ Warning: Context MAJOR ({context_major}) doesn't match code MAJOR ({new_major})")
|
|
605
|
+
print(f" Updating context to match...")
|
|
606
|
+
|
|
607
|
+
new_context_version = f"{new_major}.0"
|
|
608
|
+
|
|
609
|
+
# Update versions
|
|
610
|
+
today = date.today().isoformat()
|
|
611
|
+
data['versioning']['code_version'] = code_version
|
|
612
|
+
data['versioning']['context_version'] = new_context_version
|
|
613
|
+
data['versioning']['last_updated'] = today
|
|
614
|
+
|
|
615
|
+
# Add change entry
|
|
616
|
+
if 'changes' not in data:
|
|
617
|
+
data['changes'] = []
|
|
618
|
+
|
|
619
|
+
# Get Git information if available
|
|
620
|
+
commit_hash = get_git_commit_hash()
|
|
621
|
+
author = get_git_author()
|
|
622
|
+
|
|
623
|
+
change_entry = {
|
|
624
|
+
'type': 'context',
|
|
625
|
+
'description': f"Synced with v{code_version}",
|
|
626
|
+
'date': today,
|
|
627
|
+
'context_version': new_context_version,
|
|
628
|
+
'code_version': code_version
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if commit_hash:
|
|
632
|
+
change_entry['commit'] = commit_hash
|
|
633
|
+
if author:
|
|
634
|
+
change_entry['author'] = author
|
|
635
|
+
|
|
636
|
+
data['changes'].append(change_entry)
|
|
637
|
+
|
|
638
|
+
# Write updated context
|
|
639
|
+
from .yaml_utils import parse_context_file
|
|
640
|
+
context = parse_context_file(data)
|
|
641
|
+
write_context_yaml(context, context_path)
|
|
642
|
+
|
|
643
|
+
print(f"✅ Context synced to {new_context_version}")
|
|
644
|
+
print(f" Code version updated to {code_version}")
|
|
645
|
+
print()
|
|
646
|
+
|
|
647
|
+
else:
|
|
648
|
+
# No major version change - just update code version
|
|
649
|
+
data['versioning']['code_version'] = code_version
|
|
650
|
+
data['versioning']['last_updated'] = date.today().isoformat()
|
|
651
|
+
|
|
652
|
+
# Write updated context
|
|
653
|
+
from .yaml_utils import parse_context_file
|
|
654
|
+
context = parse_context_file(data)
|
|
655
|
+
write_context_yaml(context, context_path)
|
|
656
|
+
|
|
657
|
+
print(f"✅ Code version updated to {code_version}")
|
|
658
|
+
print(f" Context version unchanged: {current_context_version}")
|
|
659
|
+
print()
|
|
660
|
+
|
|
661
|
+
return 0
|
|
662
|
+
|
|
663
|
+
except Exception as e:
|
|
664
|
+
print(f"❌ Error syncing context: {e}")
|
|
665
|
+
import traceback
|
|
666
|
+
traceback.print_exc()
|
|
667
|
+
return 1
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def cmd_context_history() -> int:
|
|
671
|
+
"""
|
|
672
|
+
Show the version history of the context file.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Exit code (0 for success, 1 for error)
|
|
676
|
+
"""
|
|
677
|
+
if not context_file_exists():
|
|
678
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
679
|
+
print(" Run 'voodocs context init' to create one.")
|
|
680
|
+
return 1
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
# Read current context
|
|
684
|
+
context_path = get_context_file_path()
|
|
685
|
+
data = read_context_yaml(context_path)
|
|
686
|
+
|
|
687
|
+
if not data:
|
|
688
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
689
|
+
return 1
|
|
690
|
+
|
|
691
|
+
# Get changes
|
|
692
|
+
changes = data.get('changes', [])
|
|
693
|
+
|
|
694
|
+
if not changes:
|
|
695
|
+
print("📜 No change history found.")
|
|
696
|
+
print(" Changes will be recorded when you run 'voodocs context update'.")
|
|
697
|
+
return 0
|
|
698
|
+
|
|
699
|
+
print("📜 Context Version History")
|
|
700
|
+
print()
|
|
701
|
+
|
|
702
|
+
# Display changes in reverse chronological order
|
|
703
|
+
for change in reversed(changes):
|
|
704
|
+
change_type = change.get('type', 'unknown')
|
|
705
|
+
description = change.get('description', 'No description')
|
|
706
|
+
date = change.get('date', 'Unknown date')
|
|
707
|
+
context_version = change.get('context_version', '?')
|
|
708
|
+
code_version = change.get('code_version', '?')
|
|
709
|
+
commit = change.get('commit', '')
|
|
710
|
+
author = change.get('author', '')
|
|
711
|
+
|
|
712
|
+
# Format output
|
|
713
|
+
print(f"📌 Context v{context_version} (Code v{code_version})")
|
|
714
|
+
print(f" Date: {date}")
|
|
715
|
+
print(f" Type: {change_type}")
|
|
716
|
+
print(f" Description: {description}")
|
|
717
|
+
|
|
718
|
+
if commit:
|
|
719
|
+
print(f" Commit: {commit}")
|
|
720
|
+
if author:
|
|
721
|
+
print(f" Author: {author}")
|
|
722
|
+
|
|
723
|
+
print()
|
|
724
|
+
|
|
725
|
+
return 0
|
|
726
|
+
|
|
727
|
+
except Exception as e:
|
|
728
|
+
print(f"❌ Error reading history: {e}")
|
|
729
|
+
return 1
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def cmd_context_diff(version1: Optional[str] = None, version2: Optional[str] = None) -> int:
|
|
733
|
+
"""
|
|
734
|
+
Compare two versions of the context (currently shows change log between versions).
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
version1: First version to compare (older)
|
|
738
|
+
version2: Second version to compare (newer, default: current)
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Exit code (0 for success, 1 for error)
|
|
742
|
+
"""
|
|
743
|
+
if not context_file_exists():
|
|
744
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
745
|
+
print(" Run 'voodocs context init' to create one.")
|
|
746
|
+
return 1
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
# Read current context
|
|
750
|
+
context_path = get_context_file_path()
|
|
751
|
+
data = read_context_yaml(context_path)
|
|
752
|
+
|
|
753
|
+
if not data:
|
|
754
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
755
|
+
return 1
|
|
756
|
+
|
|
757
|
+
# Get current version
|
|
758
|
+
versioning = data.get('versioning', {})
|
|
759
|
+
current_version = versioning.get('context_version', '0.0')
|
|
760
|
+
|
|
761
|
+
# Default version2 to current
|
|
762
|
+
if not version2:
|
|
763
|
+
version2 = current_version
|
|
764
|
+
|
|
765
|
+
# If no version1 provided, show changes from previous version
|
|
766
|
+
if not version1:
|
|
767
|
+
changes = data.get('changes', [])
|
|
768
|
+
if len(changes) < 2:
|
|
769
|
+
print("📊 Not enough version history to compare.")
|
|
770
|
+
print(" Need at least 2 versions. Run 'voodocs context history' to see available versions.")
|
|
771
|
+
return 0
|
|
772
|
+
|
|
773
|
+
# Get the two most recent versions
|
|
774
|
+
recent_changes = list(reversed(changes))[:2]
|
|
775
|
+
version2 = recent_changes[0].get('context_version', '?')
|
|
776
|
+
version1 = recent_changes[1].get('context_version', '?')
|
|
777
|
+
|
|
778
|
+
print(f"📊 Context Diff: v{version1} → v{version2}")
|
|
779
|
+
print()
|
|
780
|
+
|
|
781
|
+
# Get changes between versions
|
|
782
|
+
changes = data.get('changes', [])
|
|
783
|
+
relevant_changes = []
|
|
784
|
+
|
|
785
|
+
for change in changes:
|
|
786
|
+
change_version = change.get('context_version', '')
|
|
787
|
+
|
|
788
|
+
# Parse versions for comparison
|
|
789
|
+
try:
|
|
790
|
+
v1_parts = [int(x) for x in version1.split('.')]
|
|
791
|
+
v2_parts = [int(x) for x in version2.split('.')]
|
|
792
|
+
cv_parts = [int(x) for x in change_version.split('.')]
|
|
793
|
+
|
|
794
|
+
# Check if change is between v1 and v2
|
|
795
|
+
if v1_parts < cv_parts <= v2_parts:
|
|
796
|
+
relevant_changes.append(change)
|
|
797
|
+
except (ValueError, AttributeError):
|
|
798
|
+
# Skip if version parsing fails
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
if not relevant_changes:
|
|
802
|
+
print(f"No changes found between v{version1} and v{version2}")
|
|
803
|
+
return 0
|
|
804
|
+
|
|
805
|
+
print(f"Changes ({len(relevant_changes)}):")
|
|
806
|
+
print()
|
|
807
|
+
|
|
808
|
+
for change in relevant_changes:
|
|
809
|
+
change_type = change.get('type', 'unknown')
|
|
810
|
+
description = change.get('description', 'No description')
|
|
811
|
+
date = change.get('date', 'Unknown date')
|
|
812
|
+
context_version = change.get('context_version', '?')
|
|
813
|
+
code_version = change.get('code_version', '?')
|
|
814
|
+
|
|
815
|
+
print(f" • v{context_version} ({date})")
|
|
816
|
+
print(f" {description}")
|
|
817
|
+
if code_version:
|
|
818
|
+
print(f" Code: v{code_version}")
|
|
819
|
+
print()
|
|
820
|
+
|
|
821
|
+
print(f"💡 Tip: Use 'voodocs context history' to see the full timeline.")
|
|
822
|
+
|
|
823
|
+
return 0
|
|
824
|
+
|
|
825
|
+
except Exception as e:
|
|
826
|
+
print(f"❌ Error comparing versions: {e}")
|
|
827
|
+
import traceback
|
|
828
|
+
traceback.print_exc()
|
|
829
|
+
return 1
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def cmd_context_validate(check_version: bool = False, check_invariants: bool = False) -> int:
|
|
833
|
+
"""
|
|
834
|
+
Validate the context file for correctness and consistency.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
check_version: Check if code version matches detected version
|
|
838
|
+
check_invariants: Check if code respects documented invariants (future)
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Exit code (0 for success, 1 for warnings, 2 for errors)
|
|
842
|
+
"""
|
|
843
|
+
if not context_file_exists():
|
|
844
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
845
|
+
print(" Run 'voodocs context init' to create one.")
|
|
846
|
+
return 2
|
|
847
|
+
|
|
848
|
+
try:
|
|
849
|
+
# Read current context
|
|
850
|
+
context_path = get_context_file_path()
|
|
851
|
+
data = read_context_yaml(context_path)
|
|
852
|
+
|
|
853
|
+
if not data:
|
|
854
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
855
|
+
return 2
|
|
856
|
+
|
|
857
|
+
print("🔍 Validating Context File")
|
|
858
|
+
print()
|
|
859
|
+
|
|
860
|
+
errors = []
|
|
861
|
+
warnings = []
|
|
862
|
+
|
|
863
|
+
# Check required fields
|
|
864
|
+
versioning = data.get('versioning', {})
|
|
865
|
+
if not versioning:
|
|
866
|
+
errors.append("Missing 'versioning' section")
|
|
867
|
+
else:
|
|
868
|
+
if not versioning.get('code_version'):
|
|
869
|
+
errors.append("Missing 'versioning.code_version'")
|
|
870
|
+
if not versioning.get('context_version'):
|
|
871
|
+
errors.append("Missing 'versioning.context_version'")
|
|
872
|
+
if not versioning.get('last_updated'):
|
|
873
|
+
warnings.append("Missing 'versioning.last_updated'")
|
|
874
|
+
|
|
875
|
+
project = data.get('project', {})
|
|
876
|
+
if not project:
|
|
877
|
+
errors.append("Missing 'project' section")
|
|
878
|
+
else:
|
|
879
|
+
if not project.get('name'):
|
|
880
|
+
errors.append("Missing 'project.name'")
|
|
881
|
+
if not project.get('purpose'):
|
|
882
|
+
warnings.append("Missing 'project.purpose'")
|
|
883
|
+
|
|
884
|
+
# Check version format
|
|
885
|
+
code_version = versioning.get('code_version', '')
|
|
886
|
+
if code_version:
|
|
887
|
+
if code_version.count('.') < 2:
|
|
888
|
+
errors.append(f"Invalid code version format: '{code_version}' (expected MAJOR.MINOR.PATCH)")
|
|
889
|
+
|
|
890
|
+
context_version = versioning.get('context_version', '')
|
|
891
|
+
if context_version:
|
|
892
|
+
if context_version.count('.') != 1:
|
|
893
|
+
errors.append(f"Invalid context version format: '{context_version}' (expected MAJOR.MINOR)")
|
|
894
|
+
|
|
895
|
+
# Check version consistency
|
|
896
|
+
if code_version and context_version:
|
|
897
|
+
code_major = code_version.split('.')[0]
|
|
898
|
+
context_major = context_version.split('.')[0]
|
|
899
|
+
|
|
900
|
+
if code_major != context_major:
|
|
901
|
+
warnings.append(
|
|
902
|
+
f"Version mismatch: code MAJOR ({code_major}) != context MAJOR ({context_major})"
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Check if code version matches detected version
|
|
906
|
+
if check_version:
|
|
907
|
+
detected_version = detect_code_version()
|
|
908
|
+
if detected_version and detected_version != code_version:
|
|
909
|
+
warnings.append(
|
|
910
|
+
f"Code version mismatch: context has '{code_version}' but detected '{detected_version}'"
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# Check invariants using the checker
|
|
914
|
+
if check_invariants:
|
|
915
|
+
invariants = data.get('invariants', {}).get('global', [])
|
|
916
|
+
if invariants:
|
|
917
|
+
from .checker import InvariantChecker
|
|
918
|
+
checker = InvariantChecker()
|
|
919
|
+
results = checker.check_invariants(invariants, get_project_root(), None)
|
|
920
|
+
|
|
921
|
+
failed = sum(1 for r in results if not r.passed)
|
|
922
|
+
if failed > 0:
|
|
923
|
+
warnings.append(f"{failed} invariant(s) have potential violations")
|
|
924
|
+
warnings.append("Run 'voodocs context check' for details")
|
|
925
|
+
|
|
926
|
+
# Display results
|
|
927
|
+
if errors:
|
|
928
|
+
print("❌ Errors:")
|
|
929
|
+
for error in errors:
|
|
930
|
+
print(f" • {error}")
|
|
931
|
+
print()
|
|
932
|
+
|
|
933
|
+
if warnings:
|
|
934
|
+
print("⚠️ Warnings:")
|
|
935
|
+
for warning in warnings:
|
|
936
|
+
print(f" • {warning}")
|
|
937
|
+
print()
|
|
938
|
+
|
|
939
|
+
# Add smart suggestions
|
|
940
|
+
suggestions = []
|
|
941
|
+
|
|
942
|
+
# Completeness suggestions
|
|
943
|
+
if not data.get('architecture', {}).get('modules'):
|
|
944
|
+
suggestions.append("Consider adding 'architecture.modules' section")
|
|
945
|
+
|
|
946
|
+
if not data.get('invariants', {}).get('global'):
|
|
947
|
+
suggestions.append("Consider adding 'invariants.global' section")
|
|
948
|
+
|
|
949
|
+
if not data.get('critical_paths'):
|
|
950
|
+
suggestions.append("Consider adding 'critical_paths' section")
|
|
951
|
+
|
|
952
|
+
if not data.get('dependencies'):
|
|
953
|
+
suggestions.append("Consider adding 'dependencies' section")
|
|
954
|
+
|
|
955
|
+
# Quality suggestions
|
|
956
|
+
invariants = data.get('invariants', {}).get('global', [])
|
|
957
|
+
for inv in invariants:
|
|
958
|
+
if len(inv) < 20:
|
|
959
|
+
suggestions.append(f"Invariant '{inv}' is too vague - be more specific")
|
|
960
|
+
break
|
|
961
|
+
|
|
962
|
+
# Consistency suggestions
|
|
963
|
+
modules = data.get('architecture', {}).get('modules', {})
|
|
964
|
+
critical_paths = data.get('critical_paths', [])
|
|
965
|
+
|
|
966
|
+
# Check if critical paths reference modules
|
|
967
|
+
for path in critical_paths:
|
|
968
|
+
path_name = path.get('name', '')
|
|
969
|
+
steps = path.get('steps', [])
|
|
970
|
+
for step in steps:
|
|
971
|
+
step_text = str(step).lower()
|
|
972
|
+
for module_name in modules.keys():
|
|
973
|
+
if module_name.lower() in step_text:
|
|
974
|
+
break
|
|
975
|
+
|
|
976
|
+
if not errors and not warnings:
|
|
977
|
+
print("✅ Context file is valid!")
|
|
978
|
+
print()
|
|
979
|
+
|
|
980
|
+
# Show summary
|
|
981
|
+
print("Summary:")
|
|
982
|
+
print(f" Project: {project.get('name', '?')}")
|
|
983
|
+
print(f" Code Version: {code_version}")
|
|
984
|
+
print(f" Context Version: {context_version}")
|
|
985
|
+
print(f" Last Updated: {versioning.get('last_updated', '?')}")
|
|
986
|
+
print()
|
|
987
|
+
|
|
988
|
+
# Show suggestions if any
|
|
989
|
+
if suggestions:
|
|
990
|
+
print("💡 Suggestions:")
|
|
991
|
+
for suggestion in suggestions[:5]: # Limit to 5
|
|
992
|
+
print(f" • {suggestion}")
|
|
993
|
+
print()
|
|
994
|
+
|
|
995
|
+
return 0
|
|
996
|
+
elif errors:
|
|
997
|
+
print(f"❌ Validation failed with {len(errors)} error(s) and {len(warnings)} warning(s)")
|
|
998
|
+
return 2
|
|
999
|
+
else:
|
|
1000
|
+
print(f"⚠️ Validation passed with {len(warnings)} warning(s)")
|
|
1001
|
+
return 1
|
|
1002
|
+
|
|
1003
|
+
except Exception as e:
|
|
1004
|
+
print(f"❌ Error validating context: {e}")
|
|
1005
|
+
import traceback
|
|
1006
|
+
traceback.print_exc()
|
|
1007
|
+
return 2
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def cmd_context_generate(source_dir: Optional[str] = None, update_existing: bool = False) -> int:
|
|
1011
|
+
"""
|
|
1012
|
+
Generate or update context by extracting information from code annotations.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
source_dir: Directory to scan for annotations (default: current directory)
|
|
1016
|
+
update_existing: Update existing context file instead of creating new one
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
Exit code (0 for success, 1 for error)
|
|
1020
|
+
"""
|
|
1021
|
+
from pathlib import Path
|
|
1022
|
+
import sys
|
|
1023
|
+
|
|
1024
|
+
# Determine source directory
|
|
1025
|
+
if source_dir:
|
|
1026
|
+
scan_path = Path(source_dir).resolve()
|
|
1027
|
+
else:
|
|
1028
|
+
scan_path = Path.cwd()
|
|
1029
|
+
|
|
1030
|
+
if not scan_path.exists():
|
|
1031
|
+
print(f"❌ Error: Directory not found: {scan_path}")
|
|
1032
|
+
return 1
|
|
1033
|
+
|
|
1034
|
+
print(f"🔍 Scanning for @voodocs annotations in: {scan_path}")
|
|
1035
|
+
print()
|
|
1036
|
+
|
|
1037
|
+
try:
|
|
1038
|
+
# Import the annotation parser
|
|
1039
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
1040
|
+
from annotations.parser import AnnotationParser
|
|
1041
|
+
|
|
1042
|
+
# Scan for annotations
|
|
1043
|
+
parser = AnnotationParser()
|
|
1044
|
+
results = parser.parse_directory(scan_path)
|
|
1045
|
+
|
|
1046
|
+
if not results:
|
|
1047
|
+
print("⚠️ No @voodocs annotations found.")
|
|
1048
|
+
print(" Add @voodocs annotations to your code first.")
|
|
1049
|
+
return 1
|
|
1050
|
+
|
|
1051
|
+
# Extract global invariants from all annotations
|
|
1052
|
+
global_invariants = set()
|
|
1053
|
+
assumptions = []
|
|
1054
|
+
modules_info = {}
|
|
1055
|
+
|
|
1056
|
+
for parsed in results:
|
|
1057
|
+
# Get module annotation
|
|
1058
|
+
module_ann = parsed.module
|
|
1059
|
+
|
|
1060
|
+
# Extract module-level invariants
|
|
1061
|
+
if hasattr(module_ann, 'invariants') and module_ann.invariants:
|
|
1062
|
+
for inv in module_ann.invariants:
|
|
1063
|
+
global_invariants.add(inv)
|
|
1064
|
+
|
|
1065
|
+
# Extract assumptions
|
|
1066
|
+
if hasattr(module_ann, 'assumptions') and module_ann.assumptions:
|
|
1067
|
+
for assumption in module_ann.assumptions:
|
|
1068
|
+
assumptions.append({
|
|
1069
|
+
'description': assumption,
|
|
1070
|
+
'source': Path(parsed.source_file).relative_to(scan_path).as_posix()
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
# Store module info
|
|
1074
|
+
if hasattr(module_ann, 'module_purpose') and module_ann.module_purpose:
|
|
1075
|
+
rel_path = Path(parsed.source_file).relative_to(scan_path).as_posix()
|
|
1076
|
+
modules_info[rel_path] = {
|
|
1077
|
+
'purpose': module_ann.module_purpose,
|
|
1078
|
+
'dependencies': getattr(module_ann, 'dependencies', [])
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
# Extract function-level invariants that might be global
|
|
1082
|
+
for func_ann in module_ann.functions:
|
|
1083
|
+
if hasattr(func_ann, 'invariants') and func_ann.invariants:
|
|
1084
|
+
for inv in func_ann.invariants:
|
|
1085
|
+
# Check if invariant looks global (mentions system-wide concepts)
|
|
1086
|
+
if any(keyword in inv.lower() for keyword in ['system', 'all', 'every', 'global', '∀']):
|
|
1087
|
+
global_invariants.add(inv)
|
|
1088
|
+
|
|
1089
|
+
# Extract class-level invariants
|
|
1090
|
+
for cls_ann in module_ann.classes:
|
|
1091
|
+
if hasattr(cls_ann, 'class_invariants') and cls_ann.class_invariants:
|
|
1092
|
+
for inv in cls_ann.class_invariants:
|
|
1093
|
+
# Check if invariant looks global
|
|
1094
|
+
if any(keyword in inv.lower() for keyword in ['system', 'all', 'every', 'global', '∀']):
|
|
1095
|
+
global_invariants.add(inv)
|
|
1096
|
+
|
|
1097
|
+
# Display findings
|
|
1098
|
+
print(f"📊 Extraction Results:")
|
|
1099
|
+
print(f" Files scanned: {len(results)}")
|
|
1100
|
+
print(f" Global invariants: {len(global_invariants)}")
|
|
1101
|
+
print(f" Assumptions: {len(assumptions)}")
|
|
1102
|
+
print(f" Modules documented: {len(modules_info)}")
|
|
1103
|
+
print()
|
|
1104
|
+
|
|
1105
|
+
if not update_existing:
|
|
1106
|
+
# Check if context already exists
|
|
1107
|
+
if context_file_exists():
|
|
1108
|
+
print("⚠️ Context file already exists.")
|
|
1109
|
+
response = prompt_user("Update existing context?", "y", required=False)
|
|
1110
|
+
if response.lower() not in ['y', 'yes']:
|
|
1111
|
+
print("Cancelled.")
|
|
1112
|
+
return 0
|
|
1113
|
+
update_existing = True
|
|
1114
|
+
|
|
1115
|
+
if update_existing:
|
|
1116
|
+
# Update existing context
|
|
1117
|
+
if not context_file_exists():
|
|
1118
|
+
print("❌ Error: No .voodocs.context file found.")
|
|
1119
|
+
print(" Run 'voodocs context init' first.")
|
|
1120
|
+
return 1
|
|
1121
|
+
|
|
1122
|
+
context_path = get_context_file_path()
|
|
1123
|
+
data = read_context_yaml(context_path)
|
|
1124
|
+
|
|
1125
|
+
if not data:
|
|
1126
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
1127
|
+
return 1
|
|
1128
|
+
|
|
1129
|
+
# Update invariants
|
|
1130
|
+
if 'invariants' not in data:
|
|
1131
|
+
data['invariants'] = {}
|
|
1132
|
+
if 'global' not in data['invariants']:
|
|
1133
|
+
data['invariants']['global'] = []
|
|
1134
|
+
|
|
1135
|
+
# Merge new invariants (avoid duplicates)
|
|
1136
|
+
existing_invs = set(data['invariants']['global'])
|
|
1137
|
+
new_invs = global_invariants - existing_invs
|
|
1138
|
+
data['invariants']['global'].extend(sorted(new_invs))
|
|
1139
|
+
|
|
1140
|
+
# Update assumptions
|
|
1141
|
+
if 'assumptions' not in data:
|
|
1142
|
+
data['assumptions'] = []
|
|
1143
|
+
|
|
1144
|
+
# Merge new assumptions
|
|
1145
|
+
existing_assumption_texts = {a.get('description', '') for a in data['assumptions']}
|
|
1146
|
+
for assumption in assumptions:
|
|
1147
|
+
if assumption['description'] not in existing_assumption_texts:
|
|
1148
|
+
data['assumptions'].append(assumption)
|
|
1149
|
+
|
|
1150
|
+
# Update modules
|
|
1151
|
+
if 'architecture' not in data:
|
|
1152
|
+
data['architecture'] = {}
|
|
1153
|
+
if 'modules' not in data['architecture']:
|
|
1154
|
+
data['architecture']['modules'] = {}
|
|
1155
|
+
|
|
1156
|
+
data['architecture']['modules'].update(modules_info)
|
|
1157
|
+
|
|
1158
|
+
# Update last_updated
|
|
1159
|
+
from datetime import date
|
|
1160
|
+
data['versioning']['last_updated'] = date.today().isoformat()
|
|
1161
|
+
|
|
1162
|
+
# Write updated context
|
|
1163
|
+
from .yaml_utils import parse_context_file
|
|
1164
|
+
context = parse_context_file(data)
|
|
1165
|
+
write_context_yaml(context, context_path)
|
|
1166
|
+
|
|
1167
|
+
print(f"✅ Context updated from code annotations")
|
|
1168
|
+
print(f" Added {len(new_invs)} new invariants")
|
|
1169
|
+
print(f" Added {len(assumptions) - len(existing_assumption_texts)} new assumptions")
|
|
1170
|
+
print()
|
|
1171
|
+
print("💡 Tip: Run 'voodocs context view' to see the updated context")
|
|
1172
|
+
|
|
1173
|
+
else:
|
|
1174
|
+
# Create new context (should not reach here, but handle gracefully)
|
|
1175
|
+
print("❌ Error: Context generation requires an existing context file.")
|
|
1176
|
+
print(" Run 'voodocs context init' first, then use 'voodocs context generate --from-code'")
|
|
1177
|
+
return 1
|
|
1178
|
+
|
|
1179
|
+
return 0
|
|
1180
|
+
|
|
1181
|
+
except ImportError as e:
|
|
1182
|
+
print(f"❌ Error: Failed to import annotation parser: {e}")
|
|
1183
|
+
print(" Make sure VooDocs is properly installed.")
|
|
1184
|
+
return 1
|
|
1185
|
+
except Exception as e:
|
|
1186
|
+
print(f"❌ Error generating context: {e}")
|
|
1187
|
+
import traceback
|
|
1188
|
+
traceback.print_exc()
|
|
1189
|
+
return 1
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def cmd_context_query(query: str, section: Optional[str] = None, format: str = 'text') -> int:
|
|
1193
|
+
"""
|
|
1194
|
+
Query the context file like a database.
|
|
1195
|
+
|
|
1196
|
+
Args:
|
|
1197
|
+
query: Search query (keyword or regex pattern)
|
|
1198
|
+
section: Specific section to search (invariants, assumptions, etc.)
|
|
1199
|
+
format: Output format ('text', 'json', 'yaml')
|
|
1200
|
+
|
|
1201
|
+
Returns:
|
|
1202
|
+
Exit code (0 for success, 1 for error)
|
|
1203
|
+
"""
|
|
1204
|
+
if not context_file_exists():
|
|
1205
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
1206
|
+
print(" Run 'voodocs context init' to create one.")
|
|
1207
|
+
return 1
|
|
1208
|
+
|
|
1209
|
+
try:
|
|
1210
|
+
import re
|
|
1211
|
+
import json
|
|
1212
|
+
|
|
1213
|
+
# Read context
|
|
1214
|
+
context_path = get_context_file_path()
|
|
1215
|
+
data = read_context_yaml(context_path)
|
|
1216
|
+
|
|
1217
|
+
if not data:
|
|
1218
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
1219
|
+
return 1
|
|
1220
|
+
|
|
1221
|
+
# Compile regex pattern (case-insensitive)
|
|
1222
|
+
try:
|
|
1223
|
+
pattern = re.compile(query, re.IGNORECASE)
|
|
1224
|
+
except re.error as e:
|
|
1225
|
+
print(f"❌ Error: Invalid regex pattern: {e}")
|
|
1226
|
+
return 1
|
|
1227
|
+
|
|
1228
|
+
# Define searchable sections
|
|
1229
|
+
searchable_sections = {
|
|
1230
|
+
'invariants': data.get('invariants', {}),
|
|
1231
|
+
'assumptions': data.get('assumptions', []),
|
|
1232
|
+
'known_issues': data.get('known_issues', []),
|
|
1233
|
+
'architecture': data.get('architecture', {}),
|
|
1234
|
+
'critical_paths': data.get('critical_paths', []),
|
|
1235
|
+
'roadmap': data.get('roadmap', []),
|
|
1236
|
+
'changes': data.get('changes', []),
|
|
1237
|
+
'performance': data.get('performance', {}),
|
|
1238
|
+
'security': data.get('security', {}),
|
|
1239
|
+
'dependencies': data.get('dependencies', {}),
|
|
1240
|
+
'testing': data.get('testing', {}),
|
|
1241
|
+
'deployment': data.get('deployment', {}),
|
|
1242
|
+
'team': data.get('team', {})
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
# Filter by section if specified
|
|
1246
|
+
if section:
|
|
1247
|
+
if section not in searchable_sections:
|
|
1248
|
+
print(f"❌ Error: Unknown section '{section}'")
|
|
1249
|
+
print(f" Available sections: {', '.join(searchable_sections.keys())}")
|
|
1250
|
+
return 1
|
|
1251
|
+
searchable_sections = {section: searchable_sections[section]}
|
|
1252
|
+
|
|
1253
|
+
# Search function
|
|
1254
|
+
def search_value(value, path=""):
|
|
1255
|
+
"""Recursively search through data structures."""
|
|
1256
|
+
results = []
|
|
1257
|
+
|
|
1258
|
+
if isinstance(value, str):
|
|
1259
|
+
if pattern.search(value):
|
|
1260
|
+
results.append((path, value))
|
|
1261
|
+
elif isinstance(value, list):
|
|
1262
|
+
for i, item in enumerate(value):
|
|
1263
|
+
results.extend(search_value(item, f"{path}[{i}]"))
|
|
1264
|
+
elif isinstance(value, dict):
|
|
1265
|
+
for key, val in value.items():
|
|
1266
|
+
new_path = f"{path}.{key}" if path else key
|
|
1267
|
+
results.extend(search_value(val, new_path))
|
|
1268
|
+
|
|
1269
|
+
return results
|
|
1270
|
+
|
|
1271
|
+
# Perform search
|
|
1272
|
+
all_results = []
|
|
1273
|
+
for section_name, section_data in searchable_sections.items():
|
|
1274
|
+
results = search_value(section_data, section_name)
|
|
1275
|
+
all_results.extend(results)
|
|
1276
|
+
|
|
1277
|
+
# Display results
|
|
1278
|
+
if not all_results:
|
|
1279
|
+
print(f"🔍 No results found for: {query}")
|
|
1280
|
+
if section:
|
|
1281
|
+
print(f" (searched in: {section})")
|
|
1282
|
+
return 0
|
|
1283
|
+
|
|
1284
|
+
if format == 'json':
|
|
1285
|
+
# JSON output
|
|
1286
|
+
output = {
|
|
1287
|
+
'query': query,
|
|
1288
|
+
'section': section,
|
|
1289
|
+
'count': len(all_results),
|
|
1290
|
+
'results': [{'path': path, 'value': value} for path, value in all_results]
|
|
1291
|
+
}
|
|
1292
|
+
print(json.dumps(output, indent=2))
|
|
1293
|
+
|
|
1294
|
+
elif format == 'yaml':
|
|
1295
|
+
# YAML output
|
|
1296
|
+
import yaml
|
|
1297
|
+
output = {
|
|
1298
|
+
'query': query,
|
|
1299
|
+
'section': section,
|
|
1300
|
+
'count': len(all_results),
|
|
1301
|
+
'results': [{'path': path, 'value': value} for path, value in all_results]
|
|
1302
|
+
}
|
|
1303
|
+
print(yaml.dump(output, default_flow_style=False))
|
|
1304
|
+
|
|
1305
|
+
else:
|
|
1306
|
+
# Text output (default)
|
|
1307
|
+
print(f"🔍 Query Results: '{query}'")
|
|
1308
|
+
if section:
|
|
1309
|
+
print(f" Section: {section}")
|
|
1310
|
+
print(f" Found: {len(all_results)} match(es)")
|
|
1311
|
+
print()
|
|
1312
|
+
|
|
1313
|
+
for path, value in all_results:
|
|
1314
|
+
print(f"📍 {path}")
|
|
1315
|
+
# Highlight the match
|
|
1316
|
+
highlighted = pattern.sub(lambda m: f"**{m.group()}**", value)
|
|
1317
|
+
print(f" {highlighted}")
|
|
1318
|
+
print()
|
|
1319
|
+
|
|
1320
|
+
return 0
|
|
1321
|
+
|
|
1322
|
+
except Exception as e:
|
|
1323
|
+
print(f"❌ Error querying context: {e}")
|
|
1324
|
+
import traceback
|
|
1325
|
+
traceback.print_exc()
|
|
1326
|
+
return 1
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def get_git_commit_hash() -> Optional[str]:
|
|
1330
|
+
"""
|
|
1331
|
+
Get the current Git commit hash.
|
|
1332
|
+
|
|
1333
|
+
Returns:
|
|
1334
|
+
Commit hash (short form) or None if not in a Git repository
|
|
1335
|
+
"""
|
|
1336
|
+
try:
|
|
1337
|
+
import subprocess
|
|
1338
|
+
result = subprocess.run(
|
|
1339
|
+
['git', 'rev-parse', '--short', 'HEAD'],
|
|
1340
|
+
capture_output=True,
|
|
1341
|
+
text=True,
|
|
1342
|
+
check=True
|
|
1343
|
+
)
|
|
1344
|
+
return result.stdout.strip()
|
|
1345
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1346
|
+
return None
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def get_git_author() -> Optional[str]:
|
|
1350
|
+
"""
|
|
1351
|
+
Get the Git author name and email.
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
Author string in format "Name <email>" or None if not in a Git repository
|
|
1355
|
+
"""
|
|
1356
|
+
try:
|
|
1357
|
+
import subprocess
|
|
1358
|
+
result = subprocess.run(
|
|
1359
|
+
['git', 'config', 'user.name'],
|
|
1360
|
+
capture_output=True,
|
|
1361
|
+
text=True,
|
|
1362
|
+
check=True
|
|
1363
|
+
)
|
|
1364
|
+
name = result.stdout.strip()
|
|
1365
|
+
|
|
1366
|
+
result = subprocess.run(
|
|
1367
|
+
['git', 'config', 'user.email'],
|
|
1368
|
+
capture_output=True,
|
|
1369
|
+
text=True,
|
|
1370
|
+
check=True
|
|
1371
|
+
)
|
|
1372
|
+
email = result.stdout.strip()
|
|
1373
|
+
|
|
1374
|
+
if name and email:
|
|
1375
|
+
return f"{name} <{email}>"
|
|
1376
|
+
elif name:
|
|
1377
|
+
return name
|
|
1378
|
+
else:
|
|
1379
|
+
return None
|
|
1380
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1381
|
+
return None
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def is_git_repository() -> bool:
|
|
1385
|
+
"""
|
|
1386
|
+
Check if the current directory is inside a Git repository.
|
|
1387
|
+
|
|
1388
|
+
Returns:
|
|
1389
|
+
True if in a Git repository, False otherwise
|
|
1390
|
+
"""
|
|
1391
|
+
try:
|
|
1392
|
+
import subprocess
|
|
1393
|
+
subprocess.run(
|
|
1394
|
+
['git', 'rev-parse', '--git-dir'],
|
|
1395
|
+
capture_output=True,
|
|
1396
|
+
check=True
|
|
1397
|
+
)
|
|
1398
|
+
return True
|
|
1399
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1400
|
+
return False
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def cmd_context_check(
|
|
1405
|
+
module_filter: Optional[str] = None,
|
|
1406
|
+
invariant_filter: Optional[str] = None,
|
|
1407
|
+
output_format: str = 'text'
|
|
1408
|
+
) -> int:
|
|
1409
|
+
"""
|
|
1410
|
+
Check that code respects documented invariants.
|
|
1411
|
+
|
|
1412
|
+
Args:
|
|
1413
|
+
module_filter: Optional module name to filter checks
|
|
1414
|
+
invariant_filter: Optional invariant text to filter checks
|
|
1415
|
+
output_format: Output format ('text', 'json')
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
Exit code (0 for all passed, 1 for violations found, 2 for error)
|
|
1419
|
+
"""
|
|
1420
|
+
if not context_file_exists():
|
|
1421
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
1422
|
+
print(" Run 'voodocs context init' to create one.")
|
|
1423
|
+
return 2
|
|
1424
|
+
|
|
1425
|
+
try:
|
|
1426
|
+
from .checker import InvariantChecker, ViolationSeverity
|
|
1427
|
+
import json
|
|
1428
|
+
|
|
1429
|
+
# Read context
|
|
1430
|
+
context_path = get_context_file_path()
|
|
1431
|
+
data = read_context_yaml(context_path)
|
|
1432
|
+
|
|
1433
|
+
if not data:
|
|
1434
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
1435
|
+
return 2
|
|
1436
|
+
|
|
1437
|
+
# Get invariants
|
|
1438
|
+
invariants_data = data.get('invariants', {})
|
|
1439
|
+
global_invariants = invariants_data.get('global', [])
|
|
1440
|
+
|
|
1441
|
+
if not global_invariants:
|
|
1442
|
+
print("⚠️ No invariants found in context file.")
|
|
1443
|
+
print(" Add invariants to the 'invariants.global' section.")
|
|
1444
|
+
return 0
|
|
1445
|
+
|
|
1446
|
+
# Filter invariants if specified
|
|
1447
|
+
if invariant_filter:
|
|
1448
|
+
global_invariants = [
|
|
1449
|
+
inv for inv in global_invariants
|
|
1450
|
+
if invariant_filter.lower() in inv.lower()
|
|
1451
|
+
]
|
|
1452
|
+
|
|
1453
|
+
if not global_invariants:
|
|
1454
|
+
print(f"⚠️ No invariants match filter: {invariant_filter}")
|
|
1455
|
+
return 0
|
|
1456
|
+
|
|
1457
|
+
# Get source directory
|
|
1458
|
+
source_dir = get_project_root()
|
|
1459
|
+
|
|
1460
|
+
# Initialize checker
|
|
1461
|
+
checker = InvariantChecker()
|
|
1462
|
+
|
|
1463
|
+
if output_format == 'text':
|
|
1464
|
+
print("🔍 Checking Invariants")
|
|
1465
|
+
print()
|
|
1466
|
+
|
|
1467
|
+
# Check each invariant
|
|
1468
|
+
results = checker.check_invariants(
|
|
1469
|
+
global_invariants,
|
|
1470
|
+
source_dir,
|
|
1471
|
+
module_filter
|
|
1472
|
+
)
|
|
1473
|
+
|
|
1474
|
+
# Output results
|
|
1475
|
+
if output_format == 'json':
|
|
1476
|
+
output = {
|
|
1477
|
+
'total': len(results),
|
|
1478
|
+
'passed': sum(1 for r in results if r.passed),
|
|
1479
|
+
'failed': sum(1 for r in results if not r.passed),
|
|
1480
|
+
'results': []
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
for result in results:
|
|
1484
|
+
output['results'].append({
|
|
1485
|
+
'invariant': result.invariant,
|
|
1486
|
+
'passed': result.passed,
|
|
1487
|
+
'checked_files': result.checked_files,
|
|
1488
|
+
'violations': [
|
|
1489
|
+
{
|
|
1490
|
+
'file': v.file_path,
|
|
1491
|
+
'line': v.line_number,
|
|
1492
|
+
'content': v.line_content,
|
|
1493
|
+
'severity': v.severity.value,
|
|
1494
|
+
'explanation': v.explanation
|
|
1495
|
+
}
|
|
1496
|
+
for v in result.violations
|
|
1497
|
+
]
|
|
1498
|
+
})
|
|
1499
|
+
|
|
1500
|
+
print(json.dumps(output, indent=2))
|
|
1501
|
+
|
|
1502
|
+
else:
|
|
1503
|
+
# Text output
|
|
1504
|
+
passed_count = 0
|
|
1505
|
+
failed_count = 0
|
|
1506
|
+
warning_count = 0
|
|
1507
|
+
|
|
1508
|
+
for result in results:
|
|
1509
|
+
if result.passed:
|
|
1510
|
+
passed_count += 1
|
|
1511
|
+
print(f"✅ {result.invariant}")
|
|
1512
|
+
print(f" No violations found (checked {result.checked_files} files)")
|
|
1513
|
+
print()
|
|
1514
|
+
else:
|
|
1515
|
+
# Count by severity
|
|
1516
|
+
errors = [v for v in result.violations if v.severity == ViolationSeverity.ERROR]
|
|
1517
|
+
warnings = [v for v in result.violations if v.severity == ViolationSeverity.WARNING]
|
|
1518
|
+
infos = [v for v in result.violations if v.severity == ViolationSeverity.INFO]
|
|
1519
|
+
|
|
1520
|
+
if errors:
|
|
1521
|
+
failed_count += 1
|
|
1522
|
+
print(f"❌ {result.invariant}")
|
|
1523
|
+
elif warnings:
|
|
1524
|
+
warning_count += 1
|
|
1525
|
+
print(f"⚠️ {result.invariant}")
|
|
1526
|
+
else:
|
|
1527
|
+
print(f"ℹ️ {result.invariant}")
|
|
1528
|
+
|
|
1529
|
+
print(f" Found {len(result.violations)} potential violation(s):")
|
|
1530
|
+
print()
|
|
1531
|
+
|
|
1532
|
+
# Show first 5 violations
|
|
1533
|
+
for violation in result.violations[:5]:
|
|
1534
|
+
print(f" 📍 {violation.file_path}:{violation.line_number}")
|
|
1535
|
+
print(f" {violation.line_content}")
|
|
1536
|
+
print(f" → {violation.explanation}")
|
|
1537
|
+
print()
|
|
1538
|
+
|
|
1539
|
+
if len(result.violations) > 5:
|
|
1540
|
+
print(f" ... and {len(result.violations) - 5} more")
|
|
1541
|
+
print()
|
|
1542
|
+
|
|
1543
|
+
# Summary
|
|
1544
|
+
print("=" * 60)
|
|
1545
|
+
print("Summary:")
|
|
1546
|
+
print(f" ✅ Passed: {passed_count}")
|
|
1547
|
+
if failed_count > 0:
|
|
1548
|
+
print(f" ❌ Failed: {failed_count}")
|
|
1549
|
+
if warning_count > 0:
|
|
1550
|
+
print(f" ⚠️ Warnings: {warning_count}")
|
|
1551
|
+
print()
|
|
1552
|
+
|
|
1553
|
+
# Return exit code
|
|
1554
|
+
if any(not r.passed for r in results):
|
|
1555
|
+
return 1
|
|
1556
|
+
return 0
|
|
1557
|
+
|
|
1558
|
+
except ImportError as e:
|
|
1559
|
+
print(f"❌ Error: Failed to import checker: {e}")
|
|
1560
|
+
return 2
|
|
1561
|
+
except Exception as e:
|
|
1562
|
+
print(f"❌ Error checking invariants: {e}")
|
|
1563
|
+
import traceback
|
|
1564
|
+
traceback.print_exc()
|
|
1565
|
+
return 2
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def cmd_context_diagram(
|
|
1570
|
+
diagram_type: str = 'modules',
|
|
1571
|
+
output_format: str = 'mermaid',
|
|
1572
|
+
output_file: Optional[str] = None,
|
|
1573
|
+
render_png: bool = False
|
|
1574
|
+
) -> int:
|
|
1575
|
+
"""
|
|
1576
|
+
Generate architecture diagrams from context.
|
|
1577
|
+
|
|
1578
|
+
Args:
|
|
1579
|
+
diagram_type: Type of diagram ('modules', 'dependencies', 'flow', 'all')
|
|
1580
|
+
output_format: Output format ('mermaid', 'd2')
|
|
1581
|
+
output_file: Output file path (optional)
|
|
1582
|
+
render_png: Render to PNG using manus-render-diagram
|
|
1583
|
+
|
|
1584
|
+
Returns:
|
|
1585
|
+
Exit code (0 for success, 1 for error)
|
|
1586
|
+
"""
|
|
1587
|
+
if not context_file_exists():
|
|
1588
|
+
print("❌ Error: No .voodocs.context file found in this directory.")
|
|
1589
|
+
print(" Run 'voodocs context init' to create one.")
|
|
1590
|
+
return 1
|
|
1591
|
+
|
|
1592
|
+
try:
|
|
1593
|
+
from .diagram import DiagramGenerator
|
|
1594
|
+
|
|
1595
|
+
# Read context
|
|
1596
|
+
context_path = get_context_file_path()
|
|
1597
|
+
data = read_context_yaml(context_path)
|
|
1598
|
+
|
|
1599
|
+
if not data:
|
|
1600
|
+
print("❌ Error: Failed to read .voodocs.context file.")
|
|
1601
|
+
return 1
|
|
1602
|
+
|
|
1603
|
+
# Initialize generator
|
|
1604
|
+
generator = DiagramGenerator()
|
|
1605
|
+
|
|
1606
|
+
# Generate diagrams
|
|
1607
|
+
diagrams = {}
|
|
1608
|
+
|
|
1609
|
+
if diagram_type in ['modules', 'all']:
|
|
1610
|
+
print(f"📊 Generating module diagram...")
|
|
1611
|
+
diagrams['modules'] = generator.generate_module_diagram(data, output_format)
|
|
1612
|
+
|
|
1613
|
+
if diagram_type in ['dependencies', 'all']:
|
|
1614
|
+
print(f"📊 Generating dependency diagram...")
|
|
1615
|
+
diagrams['dependencies'] = generator.generate_dependency_diagram(data, output_format)
|
|
1616
|
+
|
|
1617
|
+
if diagram_type in ['flow', 'all']:
|
|
1618
|
+
print(f"📊 Generating flow diagram...")
|
|
1619
|
+
diagrams['flow'] = generator.generate_flow_diagram(data, output_format)
|
|
1620
|
+
|
|
1621
|
+
if not diagrams:
|
|
1622
|
+
print(f"❌ Error: Unknown diagram type: {diagram_type}")
|
|
1623
|
+
print(" Valid types: modules, dependencies, flow, all")
|
|
1624
|
+
return 1
|
|
1625
|
+
|
|
1626
|
+
# Output diagrams
|
|
1627
|
+
if output_file:
|
|
1628
|
+
output_path = Path(output_file)
|
|
1629
|
+
|
|
1630
|
+
if diagram_type == 'all':
|
|
1631
|
+
# Save multiple diagrams
|
|
1632
|
+
for dtype, diagram_source in diagrams.items():
|
|
1633
|
+
dtype_path = output_path.parent / f"{output_path.stem}_{dtype}{output_path.suffix}"
|
|
1634
|
+
|
|
1635
|
+
if render_png:
|
|
1636
|
+
# Render to PNG
|
|
1637
|
+
png_path = dtype_path.with_suffix('.png')
|
|
1638
|
+
success = generator.render_to_png(diagram_source, png_path, output_format)
|
|
1639
|
+
|
|
1640
|
+
if success:
|
|
1641
|
+
print(f"✅ {dtype.capitalize()} diagram saved to: {png_path}")
|
|
1642
|
+
else:
|
|
1643
|
+
print(f"❌ Failed to render {dtype} diagram")
|
|
1644
|
+
# Still save source
|
|
1645
|
+
dtype_path.write_text(diagram_source)
|
|
1646
|
+
print(f" Source saved to: {dtype_path}")
|
|
1647
|
+
else:
|
|
1648
|
+
# Save source only
|
|
1649
|
+
dtype_path.write_text(diagram_source)
|
|
1650
|
+
print(f"✅ {dtype.capitalize()} diagram saved to: {dtype_path}")
|
|
1651
|
+
else:
|
|
1652
|
+
# Save single diagram
|
|
1653
|
+
if render_png:
|
|
1654
|
+
# Render to PNG
|
|
1655
|
+
png_path = output_path.with_suffix('.png')
|
|
1656
|
+
success = generator.render_to_png(diagrams[diagram_type], png_path, output_format)
|
|
1657
|
+
|
|
1658
|
+
if success:
|
|
1659
|
+
print(f"✅ Diagram saved to: {png_path}")
|
|
1660
|
+
else:
|
|
1661
|
+
print(f"❌ Failed to render diagram")
|
|
1662
|
+
# Still save source
|
|
1663
|
+
output_path.write_text(diagrams[diagram_type])
|
|
1664
|
+
print(f" Source saved to: {output_path}")
|
|
1665
|
+
else:
|
|
1666
|
+
# Save source only
|
|
1667
|
+
output_path.write_text(diagrams[diagram_type])
|
|
1668
|
+
print(f"✅ Diagram saved to: {output_path}")
|
|
1669
|
+
else:
|
|
1670
|
+
# Print to console
|
|
1671
|
+
for dtype, diagram_source in diagrams.items():
|
|
1672
|
+
if len(diagrams) > 1:
|
|
1673
|
+
print(f"\n{'=' * 60}")
|
|
1674
|
+
print(f"{dtype.upper()} DIAGRAM")
|
|
1675
|
+
print('=' * 60)
|
|
1676
|
+
print(diagram_source)
|
|
1677
|
+
print()
|
|
1678
|
+
|
|
1679
|
+
return 0
|
|
1680
|
+
|
|
1681
|
+
except ImportError as e:
|
|
1682
|
+
print(f"❌ Error: Failed to import diagram generator: {e}")
|
|
1683
|
+
return 1
|
|
1684
|
+
except Exception as e:
|
|
1685
|
+
print(f"❌ Error generating diagram: {e}")
|
|
1686
|
+
import traceback
|
|
1687
|
+
traceback.print_exc()
|
|
1688
|
+
return 1
|