@voodocs/cli 0.1.2 → 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.
@@ -0,0 +1,300 @@
1
+ """
2
+ Architecture Diagram Generator
3
+
4
+ Generates visual diagrams from context files.
5
+ """
6
+
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Any
10
+
11
+
12
+ class DiagramGenerator:
13
+ """Generates architecture diagrams from context."""
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def generate_module_diagram(
19
+ self,
20
+ context_data: Dict[str, Any],
21
+ format: str = 'mermaid'
22
+ ) -> str:
23
+ """
24
+ Generate a module architecture diagram.
25
+
26
+ Args:
27
+ context_data: Context file data
28
+ format: Output format ('mermaid' or 'd2')
29
+
30
+ Returns:
31
+ Diagram source code
32
+ """
33
+ architecture = context_data.get('architecture', {})
34
+ modules = architecture.get('modules', {})
35
+
36
+ if not modules:
37
+ return "# No modules found in context"
38
+
39
+ if format == 'mermaid':
40
+ return self._generate_mermaid_modules(modules)
41
+ elif format == 'd2':
42
+ return self._generate_d2_modules(modules)
43
+ else:
44
+ raise ValueError(f"Unsupported format: {format}")
45
+
46
+ def generate_dependency_diagram(
47
+ self,
48
+ context_data: Dict[str, Any],
49
+ format: str = 'mermaid'
50
+ ) -> str:
51
+ """
52
+ Generate a dependency diagram.
53
+
54
+ Args:
55
+ context_data: Context file data
56
+ format: Output format ('mermaid' or 'd2')
57
+
58
+ Returns:
59
+ Diagram source code
60
+ """
61
+ dependencies = context_data.get('dependencies', {})
62
+
63
+ if not dependencies:
64
+ return "# No dependencies found in context"
65
+
66
+ if format == 'mermaid':
67
+ return self._generate_mermaid_dependencies(dependencies)
68
+ elif format == 'd2':
69
+ return self._generate_d2_dependencies(dependencies)
70
+ else:
71
+ raise ValueError(f"Unsupported format: {format}")
72
+
73
+ def generate_flow_diagram(
74
+ self,
75
+ context_data: Dict[str, Any],
76
+ format: str = 'mermaid'
77
+ ) -> str:
78
+ """
79
+ Generate a critical path flow diagram.
80
+
81
+ Args:
82
+ context_data: Context file data
83
+ format: Output format ('mermaid' or 'd2')
84
+
85
+ Returns:
86
+ Diagram source code
87
+ """
88
+ critical_paths = context_data.get('critical_paths', [])
89
+
90
+ if not critical_paths:
91
+ return "# No critical paths found in context"
92
+
93
+ if format == 'mermaid':
94
+ return self._generate_mermaid_flows(critical_paths)
95
+ elif format == 'd2':
96
+ return self._generate_d2_flows(critical_paths)
97
+ else:
98
+ raise ValueError(f"Unsupported format: {format}")
99
+
100
+ def _generate_mermaid_modules(self, modules: Dict[str, Any]) -> str:
101
+ """Generate Mermaid diagram for modules."""
102
+ lines = ["graph TD"]
103
+
104
+ # Create nodes for each module
105
+ for i, (module_name, module_info) in enumerate(modules.items()):
106
+ node_id = f"M{i}"
107
+ purpose = module_info.get('purpose', 'No description')
108
+
109
+ # Truncate long purposes
110
+ if len(purpose) > 40:
111
+ purpose = purpose[:37] + "..."
112
+
113
+ lines.append(f" {node_id}[\"{module_name}<br/>{purpose}\"]")
114
+
115
+ # Add dependencies as edges
116
+ for i, (module_name, module_info) in enumerate(modules.items()):
117
+ node_id = f"M{i}"
118
+ deps = module_info.get('dependencies', [])
119
+
120
+ if isinstance(deps, list):
121
+ for dep in deps:
122
+ # Find the dependency module
123
+ for j, (dep_name, _) in enumerate(modules.items()):
124
+ if dep_name == dep or dep in dep_name:
125
+ dep_id = f"M{j}"
126
+ lines.append(f" {node_id} --> {dep_id}")
127
+
128
+ return "\n".join(lines)
129
+
130
+ def _generate_d2_modules(self, modules: Dict[str, Any]) -> str:
131
+ """Generate D2 diagram for modules."""
132
+ lines = []
133
+
134
+ # Create nodes for each module
135
+ for module_name, module_info in modules.items():
136
+ purpose = module_info.get('purpose', 'No description')
137
+ lines.append(f"{module_name}: {{")
138
+ lines.append(f" label: \"{purpose}\"")
139
+ lines.append("}")
140
+
141
+ # Add dependencies as edges
142
+ for module_name, module_info in modules.items():
143
+ deps = module_info.get('dependencies', [])
144
+
145
+ if isinstance(deps, list):
146
+ for dep in deps:
147
+ # Find the dependency module
148
+ if dep in modules:
149
+ lines.append(f"{module_name} -> {dep}")
150
+
151
+ return "\n".join(lines)
152
+
153
+ def _generate_mermaid_dependencies(self, dependencies: Dict[str, Any]) -> str:
154
+ """Generate Mermaid diagram for dependencies."""
155
+ lines = ["graph LR"]
156
+
157
+ project = "Project"
158
+ lines.append(f" {project}[\"Project\"]")
159
+
160
+ # Add external dependencies
161
+ external_deps = dependencies.get('external', [])
162
+ if isinstance(external_deps, list):
163
+ for i, dep in enumerate(external_deps):
164
+ dep_id = f"D{i}"
165
+ if isinstance(dep, dict):
166
+ dep_name = dep.get('name', 'Unknown')
167
+ dep_version = dep.get('version', '')
168
+ label = f"{dep_name} {dep_version}".strip()
169
+ else:
170
+ label = str(dep)
171
+
172
+ lines.append(f" {dep_id}[\"{label}\"]")
173
+ lines.append(f" {project} --> {dep_id}")
174
+
175
+ return "\n".join(lines)
176
+
177
+ def _generate_d2_dependencies(self, dependencies: Dict[str, Any]) -> str:
178
+ """Generate D2 diagram for dependencies."""
179
+ lines = ["Project"]
180
+
181
+ # Add external dependencies
182
+ external_deps = dependencies.get('external', [])
183
+ if isinstance(external_deps, list):
184
+ for dep in external_deps:
185
+ if isinstance(dep, dict):
186
+ dep_name = dep.get('name', 'Unknown')
187
+ dep_version = dep.get('version', '')
188
+ label = f"{dep_name} {dep_version}".strip()
189
+ else:
190
+ label = str(dep)
191
+
192
+ lines.append(f"Project -> {label}")
193
+
194
+ return "\n".join(lines)
195
+
196
+ def _generate_mermaid_flows(self, critical_paths: List[Dict[str, Any]]) -> str:
197
+ """Generate Mermaid diagram for critical paths."""
198
+ lines = ["graph TD"]
199
+
200
+ for path_idx, path in enumerate(critical_paths):
201
+ path_name = path.get('name', f'Path {path_idx + 1}')
202
+ steps = path.get('steps', [])
203
+
204
+ # Add path title
205
+ lines.append(f" subgraph \"{path_name}\"")
206
+
207
+ # Add steps
208
+ for step_idx, step in enumerate(steps):
209
+ node_id = f"P{path_idx}S{step_idx}"
210
+
211
+ # Truncate long steps
212
+ step_text = str(step)
213
+ if len(step_text) > 40:
214
+ step_text = step_text[:37] + "..."
215
+
216
+ lines.append(f" {node_id}[\"{step_text}\"]")
217
+
218
+ # Connect to previous step
219
+ if step_idx > 0:
220
+ prev_id = f"P{path_idx}S{step_idx - 1}"
221
+ lines.append(f" {prev_id} --> {node_id}")
222
+
223
+ lines.append(" end")
224
+
225
+ return "\n".join(lines)
226
+
227
+ def _generate_d2_flows(self, critical_paths: List[Dict[str, Any]]) -> str:
228
+ """Generate D2 diagram for critical paths."""
229
+ lines = []
230
+
231
+ for path_idx, path in enumerate(critical_paths):
232
+ path_name = path.get('name', f'Path {path_idx + 1}')
233
+ steps = path.get('steps', [])
234
+
235
+ # Add path container
236
+ lines.append(f"{path_name}: {{")
237
+
238
+ # Add steps
239
+ for step_idx, step in enumerate(steps):
240
+ node_id = f"step{step_idx + 1}"
241
+ step_text = str(step)
242
+ lines.append(f" {node_id}: \"{step_text}\"")
243
+
244
+ # Connect to previous step
245
+ if step_idx > 0:
246
+ prev_id = f"step{step_idx}"
247
+ lines.append(f" {prev_id} -> {node_id}")
248
+
249
+ lines.append("}")
250
+
251
+ return "\n".join(lines)
252
+
253
+ def render_to_png(self, diagram_source: str, output_path: Path, format: str = 'mermaid') -> bool:
254
+ """
255
+ Render diagram source to PNG using manus-render-diagram.
256
+
257
+ Args:
258
+ diagram_source: Diagram source code
259
+ output_path: Output PNG file path
260
+ format: Diagram format ('mermaid' or 'd2')
261
+
262
+ Returns:
263
+ True if successful, False otherwise
264
+ """
265
+ # Determine file extension
266
+ if format == 'mermaid':
267
+ ext = '.mmd'
268
+ elif format == 'd2':
269
+ ext = '.d2'
270
+ else:
271
+ raise ValueError(f"Unsupported format: {format}")
272
+
273
+ # Create temporary source file
274
+ temp_source = output_path.parent / f"{output_path.stem}{ext}"
275
+ temp_source.write_text(diagram_source)
276
+
277
+ try:
278
+ # Render using manus-render-diagram
279
+ result = subprocess.run(
280
+ ['manus-render-diagram', str(temp_source), str(output_path)],
281
+ capture_output=True,
282
+ text=True,
283
+ check=True
284
+ )
285
+
286
+ return True
287
+
288
+ except subprocess.CalledProcessError as e:
289
+ print(f"Error rendering diagram: {e.stderr}")
290
+ return False
291
+
292
+ except FileNotFoundError:
293
+ print("Error: manus-render-diagram not found")
294
+ print("This utility should be available in the Manus sandbox")
295
+ return False
296
+
297
+ finally:
298
+ # Clean up temporary file
299
+ if temp_source.exists():
300
+ temp_source.unlink()
@@ -0,0 +1,200 @@
1
+ """
2
+ Context System Data Models
3
+
4
+ This module defines the data structures for the VooDocs context system.
5
+ """
6
+
7
+ from dataclasses import dataclass, field, asdict
8
+ from typing import List, Dict, Optional, Any
9
+ from datetime import date
10
+
11
+
12
+ @dataclass
13
+ class Versioning:
14
+ """Version information for code and context."""
15
+ code_version: str
16
+ context_version: str
17
+ last_updated: str # ISO 8601 date (YYYY-MM-DD)
18
+
19
+
20
+ @dataclass
21
+ class Project:
22
+ """Project overview information."""
23
+ name: str
24
+ purpose: str
25
+ repository: Optional[str] = None
26
+ homepage: Optional[str] = None
27
+ license: Optional[str] = None
28
+
29
+
30
+ @dataclass
31
+ class ArchitectureDecision:
32
+ """An architectural decision record."""
33
+ decision: str
34
+ rationale: str
35
+ alternatives_considered: List[str] = field(default_factory=list)
36
+ tradeoffs: Optional[str] = None
37
+ date: Optional[str] = None
38
+ author: Optional[str] = None
39
+
40
+
41
+ @dataclass
42
+ class Module:
43
+ """A module in the system architecture."""
44
+ description: str
45
+ path: Optional[str] = None
46
+ language: Optional[str] = None
47
+ dependencies: List[str] = field(default_factory=list)
48
+ public_api: List[str] = field(default_factory=list)
49
+
50
+
51
+ @dataclass
52
+ class Architecture:
53
+ """Architecture information."""
54
+ decisions: List[ArchitectureDecision] = field(default_factory=list)
55
+ modules: Dict[str, Module] = field(default_factory=dict)
56
+ tech_stack: Dict[str, List[str]] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class CriticalPath:
61
+ """A critical workflow or data flow."""
62
+ name: str
63
+ steps: List[str]
64
+ description: Optional[str] = None
65
+ entry_point: Optional[str] = None
66
+ exit_point: Optional[str] = None
67
+
68
+
69
+ @dataclass
70
+ class KnownIssue:
71
+ """A known bug or limitation."""
72
+ issue: str
73
+ impact: str
74
+ workaround: Optional[str] = None
75
+ priority: str = "medium" # low, medium, high, critical
76
+ tracking: Optional[str] = None
77
+ discovered: Optional[str] = None
78
+
79
+
80
+ @dataclass
81
+ class Assumption:
82
+ """An assumption the system makes."""
83
+ assumption: str
84
+ rationale: Optional[str] = None
85
+ risk_if_false: Optional[str] = None
86
+
87
+
88
+ @dataclass
89
+ class Change:
90
+ """A change to the codebase or context."""
91
+ type: str # feat, fix, docs, refactor, test, context
92
+ description: str
93
+ date: str
94
+ commit: Optional[str] = None
95
+ author: Optional[str] = None
96
+ context_version: Optional[str] = None
97
+ code_version: Optional[str] = None
98
+
99
+
100
+ @dataclass
101
+ class RoadmapItem:
102
+ """A planned feature or improvement."""
103
+ feature: str
104
+ priority: str = "medium" # low, medium, high
105
+ target_version: Optional[str] = None
106
+ estimated_effort: Optional[str] = None
107
+
108
+
109
+ @dataclass
110
+ class ContextFile:
111
+ """Complete context file structure."""
112
+ versioning: Versioning
113
+ project: Project
114
+ invariants: Dict[str, Any] = field(default_factory=dict)
115
+ architecture: Architecture = field(default_factory=Architecture)
116
+ critical_paths: List[CriticalPath] = field(default_factory=list)
117
+ known_issues: List[KnownIssue] = field(default_factory=list)
118
+ assumptions: List[Assumption] = field(default_factory=list)
119
+ changes: List[Change] = field(default_factory=list)
120
+ roadmap: List[RoadmapItem] = field(default_factory=list)
121
+ performance: Dict[str, Any] = field(default_factory=dict)
122
+ security: Dict[str, Any] = field(default_factory=dict)
123
+ dependencies: Dict[str, Any] = field(default_factory=dict)
124
+ testing: Dict[str, Any] = field(default_factory=dict)
125
+ deployment: Dict[str, Any] = field(default_factory=dict)
126
+ team: Dict[str, Any] = field(default_factory=dict)
127
+ custom: Dict[str, Any] = field(default_factory=dict)
128
+
129
+ def to_dict(self) -> Dict[str, Any]:
130
+ """Convert to dictionary, excluding empty sections."""
131
+ data = asdict(self)
132
+
133
+ # Remove empty optional sections
134
+ optional_sections = [
135
+ 'performance', 'security', 'dependencies', 'testing',
136
+ 'deployment', 'team', 'custom'
137
+ ]
138
+
139
+ for section in optional_sections:
140
+ if not data.get(section):
141
+ del data[section]
142
+
143
+ # Remove empty lists (except changes which is always kept)
144
+ for key, value in list(data.items()):
145
+ if isinstance(value, list) and len(value) == 0 and key != 'changes':
146
+ del data[key]
147
+
148
+ return data
149
+
150
+
151
+ def create_minimal_context(
152
+ project_name: str,
153
+ project_purpose: str,
154
+ code_version: str,
155
+ repository: Optional[str] = None,
156
+ license: Optional[str] = None
157
+ ) -> ContextFile:
158
+ """
159
+ Create a minimal context file with required fields only.
160
+
161
+ Args:
162
+ project_name: Name of the project
163
+ project_purpose: One-sentence description of the project
164
+ code_version: Current code version (e.g., "1.0.0")
165
+ repository: Optional repository URL
166
+ license: Optional license identifier
167
+
168
+ Returns:
169
+ ContextFile with minimal required data
170
+ """
171
+ today = date.today().isoformat()
172
+
173
+ # Extract major version for context version
174
+ major_version = code_version.split('.')[0]
175
+ context_version = f"{major_version}.0"
176
+
177
+ return ContextFile(
178
+ versioning=Versioning(
179
+ code_version=code_version,
180
+ context_version=context_version,
181
+ last_updated=today
182
+ ),
183
+ project=Project(
184
+ name=project_name,
185
+ purpose=project_purpose,
186
+ repository=repository,
187
+ license=license
188
+ ),
189
+ invariants={
190
+ "global": []
191
+ },
192
+ changes=[
193
+ Change(
194
+ type="context",
195
+ description="Initial context created",
196
+ date=today,
197
+ context_version=context_version
198
+ )
199
+ ]
200
+ )