@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,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
|
+
)
|