ospac 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of ospac might be problematic. Click here for more details.

@@ -0,0 +1,332 @@
1
+ """Efficient compatibility matrix storage and retrieval."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple, Set
7
+ from enum import Enum
8
+
9
+
10
+ class CompatibilityStatus(Enum):
11
+ """License compatibility status."""
12
+ COMPATIBLE = "compatible"
13
+ INCOMPATIBLE = "incompatible"
14
+ REVIEW_NEEDED = "review_needed"
15
+ UNKNOWN = "unknown"
16
+
17
+
18
+ class CompatibilityMatrix:
19
+ """
20
+ Efficient storage for license compatibility relationships.
21
+
22
+ Uses sparse matrix representation to reduce storage size.
23
+ Only stores non-default relationships (default is "unknown").
24
+ """
25
+
26
+ def __init__(self, data_dir: str = "data/compatibility"):
27
+ self.data_dir = Path(data_dir)
28
+ self.data_dir.mkdir(parents=True, exist_ok=True)
29
+
30
+ # In-memory caches
31
+ self._compatibility_cache: Dict[str, Dict[str, str]] = {}
32
+ self._category_cache: Dict[str, List[str]] = {}
33
+ self._metadata: Dict = {}
34
+
35
+ # File paths
36
+ self.metadata_file = self.data_dir / "metadata.json"
37
+ self.categories_file = self.data_dir / "categories.json"
38
+ self.relationships_dir = self.data_dir / "relationships"
39
+ self.relationships_dir.mkdir(exist_ok=True)
40
+
41
+ def build_from_full_matrix(self, full_matrix_path: str) -> None:
42
+ """
43
+ Convert full NxN matrix to efficient sparse format.
44
+
45
+ Args:
46
+ full_matrix_path: Path to full compatibility matrix JSON
47
+ """
48
+ print(f"Loading full matrix from {full_matrix_path}...")
49
+ with open(full_matrix_path, 'r') as f:
50
+ data = json.load(f)
51
+
52
+ compatibility = data.get("compatibility", {})
53
+
54
+ # Extract metadata
55
+ self._metadata = {
56
+ "version": data.get("version", "1.0"),
57
+ "generated": data.get("generated"),
58
+ "total_licenses": len(compatibility),
59
+ "format": "sparse",
60
+ "default_status": "unknown"
61
+ }
62
+
63
+ # Categorize licenses based on patterns
64
+ categories = self._categorize_licenses(list(compatibility.keys()))
65
+
66
+ # Save metadata and categories
67
+ self._save_metadata()
68
+ self._save_categories(categories)
69
+
70
+ # Process and save relationships in chunks
71
+ self._process_relationships(compatibility, categories)
72
+
73
+ def _categorize_licenses(self, license_ids: List[str]) -> Dict[str, List[str]]:
74
+ """Categorize licenses by family/type for efficient storage."""
75
+ categories = {
76
+ "gpl": [],
77
+ "lgpl": [],
78
+ "agpl": [],
79
+ "bsd": [],
80
+ "mit": [],
81
+ "apache": [],
82
+ "cc": [],
83
+ "public_domain": [],
84
+ "proprietary": [],
85
+ "other": []
86
+ }
87
+
88
+ for license_id in license_ids:
89
+ lid_lower = license_id.lower()
90
+
91
+ if "gpl-" in lid_lower and "lgpl" not in lid_lower and "agpl" not in lid_lower:
92
+ categories["gpl"].append(license_id)
93
+ elif "lgpl" in lid_lower:
94
+ categories["lgpl"].append(license_id)
95
+ elif "agpl" in lid_lower:
96
+ categories["agpl"].append(license_id)
97
+ elif "bsd" in lid_lower:
98
+ categories["bsd"].append(license_id)
99
+ elif "mit" in lid_lower:
100
+ categories["mit"].append(license_id)
101
+ elif "apache" in lid_lower:
102
+ categories["apache"].append(license_id)
103
+ elif lid_lower.startswith("cc"):
104
+ categories["cc"].append(license_id)
105
+ elif "public" in lid_lower or "unlicense" in lid_lower or lid_lower == "0bsd":
106
+ categories["public_domain"].append(license_id)
107
+ elif any(prop in lid_lower for prop in ["proprietary", "commercial", "elastic"]):
108
+ categories["proprietary"].append(license_id)
109
+ else:
110
+ categories["other"].append(license_id)
111
+
112
+ # Remove empty categories
113
+ return {k: v for k, v in categories.items() if v}
114
+
115
+ def _process_relationships(self, compatibility: Dict, categories: Dict[str, List[str]]) -> None:
116
+ """Process and store non-default relationships efficiently."""
117
+ print("Processing compatibility relationships...")
118
+
119
+ # Statistics
120
+ total_relationships = 0
121
+ stored_relationships = 0
122
+
123
+ # Process by category to create smaller files
124
+ for category, licenses in categories.items():
125
+ category_relationships = {}
126
+
127
+ for license_id in licenses:
128
+ if license_id not in compatibility:
129
+ continue
130
+
131
+ license_compat = compatibility[license_id]
132
+
133
+ # Only store non-default relationships
134
+ non_default = {
135
+ target: status
136
+ for target, status in license_compat.items()
137
+ if status and status != "unknown"
138
+ }
139
+
140
+ if non_default:
141
+ category_relationships[license_id] = non_default
142
+ stored_relationships += len(non_default)
143
+
144
+ total_relationships += len(license_compat)
145
+
146
+ # Save category relationships
147
+ if category_relationships:
148
+ category_file = self.relationships_dir / f"{category}.json"
149
+ with open(category_file, 'w') as f:
150
+ json.dump(category_relationships, f, indent=2)
151
+ print(f" Saved {category}: {len(category_relationships)} licenses")
152
+
153
+ compression_ratio = (1 - stored_relationships / total_relationships) * 100 if total_relationships > 0 else 0
154
+ print(f"\nCompression: {stored_relationships}/{total_relationships} relationships stored")
155
+ print(f"Space saved: {compression_ratio:.1f}%")
156
+
157
+ def _save_metadata(self) -> None:
158
+ """Save metadata to file."""
159
+ with open(self.metadata_file, 'w') as f:
160
+ json.dump(self._metadata, f, indent=2)
161
+
162
+ def _save_categories(self, categories: Dict[str, List[str]]) -> None:
163
+ """Save license categories to file."""
164
+ with open(self.categories_file, 'w') as f:
165
+ json.dump(categories, f, indent=2)
166
+
167
+ def load(self) -> None:
168
+ """Load sparse matrix data into memory."""
169
+ # Load metadata
170
+ if self.metadata_file.exists():
171
+ with open(self.metadata_file, 'r') as f:
172
+ self._metadata = json.load(f)
173
+
174
+ # Load categories
175
+ if self.categories_file.exists():
176
+ with open(self.categories_file, 'r') as f:
177
+ self._category_cache = json.load(f)
178
+
179
+ # Load relationships on demand (lazy loading)
180
+ self._compatibility_cache = {}
181
+
182
+ def get_compatibility(self, license1: str, license2: str) -> str:
183
+ """
184
+ Get compatibility status between two licenses.
185
+
186
+ Args:
187
+ license1: First license ID
188
+ license2: Second license ID
189
+
190
+ Returns:
191
+ Compatibility status
192
+ """
193
+ # Check cache first
194
+ if license1 in self._compatibility_cache:
195
+ if license2 in self._compatibility_cache[license1]:
196
+ return self._compatibility_cache[license1][license2]
197
+
198
+ # Load from file if needed
199
+ status = self._load_relationship(license1, license2)
200
+
201
+ # Cache the result
202
+ if license1 not in self._compatibility_cache:
203
+ self._compatibility_cache[license1] = {}
204
+ self._compatibility_cache[license1][license2] = status
205
+
206
+ return status
207
+
208
+ def _load_relationship(self, license1: str, license2: str) -> str:
209
+ """Load specific relationship from file."""
210
+ # Find category for license1
211
+ category = self._find_category(license1)
212
+ if not category:
213
+ return self._metadata.get("default_status", "unknown")
214
+
215
+ # Load category file if not cached
216
+ category_file = self.relationships_dir / f"{category}.json"
217
+ if not category_file.exists():
218
+ return self._metadata.get("default_status", "unknown")
219
+
220
+ with open(category_file, 'r') as f:
221
+ relationships = json.load(f)
222
+
223
+ # Get relationship
224
+ if license1 in relationships:
225
+ rel = relationships[license1].get(license2)
226
+ # Handle dict format (with static/dynamic/distribution keys)
227
+ if isinstance(rel, dict):
228
+ # Return overall compatibility based on static linking (most restrictive)
229
+ static = rel.get("static_linking", "unknown")
230
+ if static == "compatible":
231
+ return "compatible"
232
+ elif static == "incompatible":
233
+ return "incompatible"
234
+ elif static == "review_required":
235
+ return "review_needed"
236
+ return static
237
+ # Handle string format
238
+ elif isinstance(rel, str):
239
+ return rel
240
+ else:
241
+ return self._metadata.get("default_status", "unknown")
242
+
243
+ return self._metadata.get("default_status", "unknown")
244
+
245
+ def _find_category(self, license_id: str) -> Optional[str]:
246
+ """Find which category a license belongs to."""
247
+ for category, licenses in self._category_cache.items():
248
+ if license_id in licenses:
249
+ return category
250
+ return None
251
+
252
+ def get_compatible_licenses(self, license_id: str) -> List[str]:
253
+ """Get all licenses compatible with the given license."""
254
+ compatible = []
255
+
256
+ # Load all relationships for this license
257
+ category = self._find_category(license_id)
258
+ if category:
259
+ category_file = self.relationships_dir / f"{category}.json"
260
+ if category_file.exists():
261
+ with open(category_file, 'r') as f:
262
+ relationships = json.load(f)
263
+
264
+ if license_id in relationships:
265
+ for target, status in relationships[license_id].items():
266
+ # Handle dict format
267
+ if isinstance(status, dict):
268
+ if status.get("static_linking") == "compatible":
269
+ compatible.append(target)
270
+ # Handle string format
271
+ elif status == CompatibilityStatus.COMPATIBLE.value:
272
+ compatible.append(target)
273
+
274
+ return sorted(compatible)
275
+
276
+ def get_incompatible_licenses(self, license_id: str) -> List[str]:
277
+ """Get all licenses incompatible with the given license."""
278
+ incompatible = []
279
+
280
+ # Load all relationships for this license
281
+ category = self._find_category(license_id)
282
+ if category:
283
+ category_file = self.relationships_dir / f"{category}.json"
284
+ if category_file.exists():
285
+ with open(category_file, 'r') as f:
286
+ relationships = json.load(f)
287
+
288
+ if license_id in relationships:
289
+ for target, status in relationships[license_id].items():
290
+ if status == CompatibilityStatus.INCOMPATIBLE.value:
291
+ incompatible.append(target)
292
+
293
+ return sorted(incompatible)
294
+
295
+ def export_full_matrix(self, output_path: str) -> None:
296
+ """Export back to full matrix format if needed."""
297
+ print("Exporting to full matrix format...")
298
+
299
+ # Load all relationships
300
+ full_compatibility = {}
301
+
302
+ for category_file in self.relationships_dir.glob("*.json"):
303
+ with open(category_file, 'r') as f:
304
+ relationships = json.load(f)
305
+ full_compatibility.update(relationships)
306
+
307
+ # Fill in defaults for all license pairs
308
+ all_licenses = []
309
+ for licenses in self._category_cache.values():
310
+ all_licenses.extend(licenses)
311
+
312
+ complete_matrix = {}
313
+ for license1 in all_licenses:
314
+ complete_matrix[license1] = {}
315
+ for license2 in all_licenses:
316
+ if license1 in full_compatibility and license2 in full_compatibility[license1]:
317
+ complete_matrix[license1][license2] = full_compatibility[license1][license2]
318
+ else:
319
+ complete_matrix[license1][license2] = self._metadata.get("default_status", "unknown")
320
+
321
+ # Save full matrix
322
+ output_data = {
323
+ "version": self._metadata.get("version", "1.0"),
324
+ "generated": self._metadata.get("generated"),
325
+ "total_licenses": len(all_licenses),
326
+ "compatibility": complete_matrix
327
+ }
328
+
329
+ with open(output_path, 'w') as f:
330
+ json.dump(output_data, f, indent=2)
331
+
332
+ print(f"Exported full matrix to {output_path}")
@@ -0,0 +1,12 @@
1
+ """Data models for OSPAC."""
2
+
3
+ from ospac.models.license import License
4
+ from ospac.models.policy import Policy
5
+ from ospac.models.compliance import ComplianceResult, PolicyResult
6
+
7
+ __all__ = [
8
+ "License",
9
+ "Policy",
10
+ "ComplianceResult",
11
+ "PolicyResult",
12
+ ]
@@ -0,0 +1,161 @@
1
+ """
2
+ Compliance result models.
3
+ """
4
+
5
+ from typing import Dict, List, Any, Optional
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+
9
+
10
+ class ComplianceStatus(Enum):
11
+ """Status of compliance check."""
12
+
13
+ COMPLIANT = "compliant"
14
+ NON_COMPLIANT = "non_compliant"
15
+ WARNING = "warning"
16
+ REQUIRES_REVIEW = "requires_review"
17
+ UNKNOWN = "unknown"
18
+
19
+
20
+ class ActionType(Enum):
21
+ """Action to take based on policy evaluation."""
22
+
23
+ ALLOW = "allow"
24
+ DENY = "deny"
25
+ FLAG_FOR_REVIEW = "flag_for_review"
26
+ CONTAMINATE = "contaminate"
27
+ APPROVE = "approve"
28
+
29
+
30
+ @dataclass
31
+ class PolicyResult:
32
+ """Result of policy evaluation."""
33
+
34
+ rule_id: str
35
+ action: ActionType
36
+ severity: str = "info"
37
+ message: Optional[str] = None
38
+ requirements: List[str] = field(default_factory=list)
39
+ remediation: Optional[str] = None
40
+
41
+ @classmethod
42
+ def aggregate(cls, results: List["PolicyResult"]) -> "PolicyResult":
43
+ """Aggregate multiple results into a single result."""
44
+ if not results:
45
+ return cls(
46
+ rule_id="aggregate",
47
+ action=ActionType.ALLOW,
48
+ severity="info",
49
+ message="No policies matched",
50
+ )
51
+
52
+ # Find the highest severity result
53
+ severity_order = {"error": 3, "warning": 2, "info": 1}
54
+ highest_severity = max(results, key=lambda r: severity_order.get(r.severity, 0))
55
+
56
+ # Determine overall action (most restrictive wins)
57
+ action_priority = {
58
+ ActionType.DENY: 5,
59
+ ActionType.CONTAMINATE: 4,
60
+ ActionType.FLAG_FOR_REVIEW: 3,
61
+ ActionType.ALLOW: 2,
62
+ ActionType.APPROVE: 1,
63
+ }
64
+
65
+ most_restrictive = max(results, key=lambda r: action_priority.get(r.action, 0))
66
+
67
+ # Combine all requirements
68
+ all_requirements = []
69
+ for result in results:
70
+ all_requirements.extend(result.requirements)
71
+
72
+ return cls(
73
+ rule_id="aggregate",
74
+ action=most_restrictive.action,
75
+ severity=highest_severity.severity,
76
+ message=f"Evaluated {len(results)} rules",
77
+ requirements=list(set(all_requirements)),
78
+ )
79
+
80
+
81
+ @dataclass
82
+ class ComplianceResult:
83
+ """Final compliance result."""
84
+
85
+ status: ComplianceStatus
86
+ licenses_checked: List[str] = field(default_factory=list)
87
+ violations: List[Dict[str, Any]] = field(default_factory=list)
88
+ warnings: List[Dict[str, Any]] = field(default_factory=list)
89
+ obligations: List[str] = field(default_factory=list)
90
+ required_actions: List[str] = field(default_factory=list)
91
+ metadata: Dict[str, Any] = field(default_factory=dict)
92
+
93
+ @property
94
+ def is_compliant(self) -> bool:
95
+ """Check if the result is compliant."""
96
+ return self.status == ComplianceStatus.COMPLIANT
97
+
98
+ @property
99
+ def needs_review(self) -> bool:
100
+ """Check if manual review is required."""
101
+ return self.status == ComplianceStatus.REQUIRES_REVIEW
102
+
103
+ def add_violation(self, rule_id: str, message: str, severity: str = "error") -> None:
104
+ """Add a violation to the result."""
105
+ self.violations.append({
106
+ "rule_id": rule_id,
107
+ "message": message,
108
+ "severity": severity,
109
+ })
110
+ if self.status != ComplianceStatus.NON_COMPLIANT:
111
+ self.status = ComplianceStatus.NON_COMPLIANT
112
+
113
+ def add_warning(self, rule_id: str, message: str) -> None:
114
+ """Add a warning to the result."""
115
+ self.warnings.append({
116
+ "rule_id": rule_id,
117
+ "message": message,
118
+ "severity": "warning",
119
+ })
120
+ if self.status == ComplianceStatus.COMPLIANT:
121
+ self.status = ComplianceStatus.WARNING
122
+
123
+ @classmethod
124
+ def from_policy_result(cls, policy_result: PolicyResult) -> "ComplianceResult":
125
+ """Create a ComplianceResult from a PolicyResult."""
126
+ result = cls(status=ComplianceStatus.UNKNOWN)
127
+
128
+ if policy_result.action == ActionType.DENY:
129
+ result.status = ComplianceStatus.NON_COMPLIANT
130
+ if policy_result.message:
131
+ result.add_violation(policy_result.rule_id, policy_result.message, policy_result.severity)
132
+
133
+ elif policy_result.action == ActionType.FLAG_FOR_REVIEW:
134
+ result.status = ComplianceStatus.REQUIRES_REVIEW
135
+ if policy_result.message:
136
+ result.add_warning(policy_result.rule_id, policy_result.message)
137
+
138
+ elif policy_result.action in [ActionType.ALLOW, ActionType.APPROVE]:
139
+ result.status = ComplianceStatus.COMPLIANT
140
+
141
+ if policy_result.requirements:
142
+ result.obligations.extend(policy_result.requirements)
143
+
144
+ if policy_result.remediation:
145
+ result.required_actions.append(policy_result.remediation)
146
+
147
+ return result
148
+
149
+ def to_dict(self) -> Dict[str, Any]:
150
+ """Convert to dictionary for serialization."""
151
+ return {
152
+ "status": self.status.value,
153
+ "is_compliant": self.is_compliant,
154
+ "needs_review": self.needs_review,
155
+ "licenses_checked": self.licenses_checked,
156
+ "violations": self.violations,
157
+ "warnings": self.warnings,
158
+ "obligations": self.obligations,
159
+ "required_actions": self.required_actions,
160
+ "metadata": self.metadata,
161
+ }
@@ -0,0 +1,82 @@
1
+ """
2
+ License data model.
3
+ """
4
+
5
+ from typing import Dict, List, Optional, Any
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class License:
11
+ """Represents a software license with its properties and requirements."""
12
+
13
+ id: str
14
+ name: str
15
+ type: str # permissive, copyleft_weak, copyleft_strong, proprietary
16
+ spdx_id: Optional[str] = None
17
+
18
+ properties: Dict[str, bool] = field(default_factory=dict)
19
+ requirements: Dict[str, bool] = field(default_factory=dict)
20
+ compatibility: Dict[str, Dict] = field(default_factory=dict)
21
+
22
+ def is_compatible_with(self, other: "License", context: str = "general") -> bool:
23
+ """Check if this license is compatible with another."""
24
+ if context not in self.compatibility:
25
+ context = "general"
26
+
27
+ if context in self.compatibility:
28
+ compat_rules = self.compatibility[context]
29
+
30
+ # Check explicit compatible list
31
+ if "compatible_with" in compat_rules:
32
+ if other.id in compat_rules["compatible_with"]:
33
+ return True
34
+ if other.type in compat_rules["compatible_with"]:
35
+ return True
36
+
37
+ # Check explicit incompatible list
38
+ if "incompatible_with" in compat_rules:
39
+ if other.id in compat_rules["incompatible_with"]:
40
+ return False
41
+ if other.type in compat_rules["incompatible_with"]:
42
+ return False
43
+
44
+ # Default: permissive licenses are generally compatible
45
+ if self.type == "permissive" and other.type == "permissive":
46
+ return True
47
+
48
+ return False
49
+
50
+ def get_obligations(self) -> List[str]:
51
+ """Get all obligations for this license."""
52
+ obligations = []
53
+
54
+ if self.requirements.get("disclose_source"):
55
+ obligations.append("Disclose source code")
56
+
57
+ if self.requirements.get("include_license"):
58
+ obligations.append("Include license text")
59
+
60
+ if self.requirements.get("include_copyright"):
61
+ obligations.append("Include copyright notice")
62
+
63
+ if self.requirements.get("state_changes"):
64
+ obligations.append("State changes made to the code")
65
+
66
+ if self.requirements.get("same_license"):
67
+ obligations.append("Distribute under same license")
68
+
69
+ return obligations
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Dict[str, Any]) -> "License":
73
+ """Create a License instance from a dictionary."""
74
+ return cls(
75
+ id=data["id"],
76
+ name=data["name"],
77
+ type=data["type"],
78
+ spdx_id=data.get("spdx_id"),
79
+ properties=data.get("properties", {}),
80
+ requirements=data.get("requirements", {}),
81
+ compatibility=data.get("compatibility", {}),
82
+ )
ospac/models/policy.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ Policy data model.
3
+ """
4
+
5
+ from typing import Dict, List, Any, Optional
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class Rule:
11
+ """Represents a single policy rule."""
12
+
13
+ id: str
14
+ description: str
15
+ when: Dict[str, Any]
16
+ then: Dict[str, Any]
17
+ priority: int = 0
18
+
19
+ def matches(self, context: Dict[str, Any]) -> bool:
20
+ """Check if this rule matches the given context."""
21
+ for key, expected in self.when.items():
22
+ if key not in context:
23
+ return False
24
+
25
+ actual = context[key]
26
+ if isinstance(expected, list):
27
+ if actual not in expected:
28
+ return False
29
+ elif actual != expected:
30
+ return False
31
+
32
+ return True
33
+
34
+
35
+ @dataclass
36
+ class Policy:
37
+ """Represents a compliance policy."""
38
+
39
+ name: str
40
+ version: str
41
+ rules: List[Rule] = field(default_factory=list)
42
+ extends: Optional[str] = None
43
+ decision_tree: Optional[List[Dict]] = None
44
+ metadata: Dict[str, Any] = field(default_factory=dict)
45
+
46
+ def add_rule(self, rule: Rule) -> None:
47
+ """Add a rule to this policy."""
48
+ self.rules.append(rule)
49
+ # Sort by priority
50
+ self.rules.sort(key=lambda r: r.priority, reverse=True)
51
+
52
+ def evaluate(self, context: Dict[str, Any]) -> List[Dict[str, Any]]:
53
+ """Evaluate this policy against the given context."""
54
+ results = []
55
+
56
+ for rule in self.rules:
57
+ if rule.matches(context):
58
+ result = {
59
+ "rule_id": rule.id,
60
+ "description": rule.description,
61
+ "action": rule.then.get("action", "allow"),
62
+ "severity": rule.then.get("severity", "info"),
63
+ "message": rule.then.get("message", ""),
64
+ }
65
+
66
+ if "requirements" in rule.then:
67
+ result["requirements"] = rule.then["requirements"]
68
+
69
+ if "remediation" in rule.then:
70
+ result["remediation"] = rule.then["remediation"]
71
+
72
+ results.append(result)
73
+
74
+ return results
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> "Policy":
78
+ """Create a Policy instance from a dictionary."""
79
+ rules = []
80
+ for rule_data in data.get("rules", []):
81
+ rule = Rule(
82
+ id=rule_data["id"],
83
+ description=rule_data.get("description", ""),
84
+ when=rule_data.get("when", {}),
85
+ then=rule_data.get("then", {}),
86
+ priority=rule_data.get("priority", 0),
87
+ )
88
+ rules.append(rule)
89
+
90
+ return cls(
91
+ name=data.get("name", "unnamed"),
92
+ version=data.get("version", "1.0"),
93
+ rules=rules,
94
+ extends=data.get("extends"),
95
+ decision_tree=data.get("decision_tree"),
96
+ metadata=data.get("metadata", {}),
97
+ )