yuho 5.0.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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/library/package.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho statute package format and validation.
|
|
3
|
+
|
|
4
|
+
Defines the .yhpkg archive format and validation for contributions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import tarfile
|
|
11
|
+
import gzip
|
|
12
|
+
import json
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Metadata schema version
|
|
20
|
+
METADATA_VERSION = "1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DeprecationInfo:
|
|
25
|
+
"""Information about package deprecation status."""
|
|
26
|
+
deprecated: bool = False
|
|
27
|
+
reason: str = ""
|
|
28
|
+
deprecated_since: str = "" # Version when deprecated
|
|
29
|
+
replacement: str = "" # Section number of replacement package (e.g., "S378A")
|
|
30
|
+
removal_version: str = "" # Version when package will be removed
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
"""Convert to dictionary for serialization."""
|
|
34
|
+
if not self.deprecated:
|
|
35
|
+
return {}
|
|
36
|
+
return {
|
|
37
|
+
"deprecated": self.deprecated,
|
|
38
|
+
"reason": self.reason,
|
|
39
|
+
"deprecated_since": self.deprecated_since,
|
|
40
|
+
"replacement": self.replacement,
|
|
41
|
+
"removal_version": self.removal_version,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DeprecationInfo":
|
|
46
|
+
"""Create from dictionary."""
|
|
47
|
+
if not data:
|
|
48
|
+
return cls()
|
|
49
|
+
return cls(
|
|
50
|
+
deprecated=data.get("deprecated", False),
|
|
51
|
+
reason=data.get("reason", ""),
|
|
52
|
+
deprecated_since=data.get("deprecated_since", ""),
|
|
53
|
+
replacement=data.get("replacement", ""),
|
|
54
|
+
removal_version=data.get("removal_version", ""),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PackageMetadata:
|
|
60
|
+
"""
|
|
61
|
+
Metadata for a Yuho statute package.
|
|
62
|
+
|
|
63
|
+
Defined in metadata.toml within the contribution directory.
|
|
64
|
+
"""
|
|
65
|
+
section_number: str
|
|
66
|
+
title: str
|
|
67
|
+
jurisdiction: str
|
|
68
|
+
contributor: str
|
|
69
|
+
version: str
|
|
70
|
+
description: str = ""
|
|
71
|
+
tags: List[str] = field(default_factory=list)
|
|
72
|
+
dependencies: List[str] = field(default_factory=list)
|
|
73
|
+
license: str = "CC-BY-4.0"
|
|
74
|
+
deprecation: DeprecationInfo = field(default_factory=DeprecationInfo)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_toml(cls, path: Path) -> "PackageMetadata":
|
|
78
|
+
"""Load metadata from TOML file."""
|
|
79
|
+
try:
|
|
80
|
+
import tomllib
|
|
81
|
+
except ImportError:
|
|
82
|
+
try:
|
|
83
|
+
import tomli as tomllib
|
|
84
|
+
except ImportError:
|
|
85
|
+
raise RuntimeError("tomllib/tomli required for metadata parsing")
|
|
86
|
+
|
|
87
|
+
with open(path, "rb") as f:
|
|
88
|
+
data = tomllib.load(f)
|
|
89
|
+
|
|
90
|
+
# Parse deprecation info if present
|
|
91
|
+
deprecation_data = data.get("deprecation", {})
|
|
92
|
+
deprecation = DeprecationInfo.from_dict(deprecation_data)
|
|
93
|
+
|
|
94
|
+
return cls(
|
|
95
|
+
section_number=data.get("section_number", ""),
|
|
96
|
+
title=data.get("title", ""),
|
|
97
|
+
jurisdiction=data.get("jurisdiction", ""),
|
|
98
|
+
contributor=data.get("contributor", ""),
|
|
99
|
+
version=data.get("version", "0.0.0"),
|
|
100
|
+
description=data.get("description", ""),
|
|
101
|
+
tags=data.get("tags", []),
|
|
102
|
+
dependencies=data.get("dependencies", []),
|
|
103
|
+
license=data.get("license", "CC-BY-4.0"),
|
|
104
|
+
deprecation=deprecation,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
108
|
+
"""Convert to dictionary."""
|
|
109
|
+
result = {
|
|
110
|
+
"section_number": self.section_number,
|
|
111
|
+
"title": self.title,
|
|
112
|
+
"jurisdiction": self.jurisdiction,
|
|
113
|
+
"contributor": self.contributor,
|
|
114
|
+
"version": self.version,
|
|
115
|
+
"description": self.description,
|
|
116
|
+
"tags": self.tags,
|
|
117
|
+
"dependencies": self.dependencies,
|
|
118
|
+
"license": self.license,
|
|
119
|
+
}
|
|
120
|
+
deprecation_dict = self.deprecation.to_dict()
|
|
121
|
+
if deprecation_dict:
|
|
122
|
+
result["deprecation"] = deprecation_dict
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def is_deprecated(self) -> bool:
|
|
127
|
+
"""Check if package is deprecated."""
|
|
128
|
+
return self.deprecation.deprecated
|
|
129
|
+
|
|
130
|
+
def get_deprecation_warning(self) -> Optional[str]:
|
|
131
|
+
"""Get deprecation warning message if deprecated."""
|
|
132
|
+
if not self.deprecation.deprecated:
|
|
133
|
+
return None
|
|
134
|
+
msg = f"Package {self.section_number} is deprecated"
|
|
135
|
+
if self.deprecation.reason:
|
|
136
|
+
msg += f": {self.deprecation.reason}"
|
|
137
|
+
if self.deprecation.replacement:
|
|
138
|
+
msg += f". Use {self.deprecation.replacement} instead"
|
|
139
|
+
if self.deprecation.removal_version:
|
|
140
|
+
msg += f". Will be removed in version {self.deprecation.removal_version}"
|
|
141
|
+
return msg
|
|
142
|
+
|
|
143
|
+
def is_valid(self) -> tuple[bool, List[str]]:
|
|
144
|
+
"""
|
|
145
|
+
Validate metadata completeness including semver validation.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (is_valid, list of error messages)
|
|
149
|
+
"""
|
|
150
|
+
from yuho.library.resolver import validate_semver
|
|
151
|
+
|
|
152
|
+
errors = []
|
|
153
|
+
|
|
154
|
+
if not self.section_number:
|
|
155
|
+
errors.append("section_number is required")
|
|
156
|
+
if not self.title:
|
|
157
|
+
errors.append("title is required")
|
|
158
|
+
if not self.jurisdiction:
|
|
159
|
+
errors.append("jurisdiction is required")
|
|
160
|
+
if not self.contributor:
|
|
161
|
+
errors.append("contributor is required")
|
|
162
|
+
if not self.version:
|
|
163
|
+
errors.append("version is required")
|
|
164
|
+
else:
|
|
165
|
+
# Validate version is proper semver
|
|
166
|
+
semver_result = validate_semver(self.version, strict=True)
|
|
167
|
+
if not semver_result.valid:
|
|
168
|
+
errors.extend(f"version: {e}" for e in semver_result.errors)
|
|
169
|
+
|
|
170
|
+
return (len(errors) == 0, errors)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class Package:
|
|
175
|
+
"""
|
|
176
|
+
A Yuho statute package (.yhpkg).
|
|
177
|
+
|
|
178
|
+
Package format:
|
|
179
|
+
- Gzipped tarball containing:
|
|
180
|
+
- statute.yh - Main statute file
|
|
181
|
+
- test_statute.yh - Test cases (optional)
|
|
182
|
+
- metadata.toml - Package metadata
|
|
183
|
+
- signature - Ed25519 signature (optional)
|
|
184
|
+
"""
|
|
185
|
+
metadata: PackageMetadata
|
|
186
|
+
statute_content: str
|
|
187
|
+
test_content: Optional[str] = None
|
|
188
|
+
signature: Optional[bytes] = None
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_directory(cls, path: Path) -> "Package":
|
|
192
|
+
"""
|
|
193
|
+
Create package from a contribution directory.
|
|
194
|
+
|
|
195
|
+
Expected structure:
|
|
196
|
+
- path/statute.yh
|
|
197
|
+
- path/test_statute.yh (optional)
|
|
198
|
+
- path/metadata.toml
|
|
199
|
+
"""
|
|
200
|
+
path = Path(path)
|
|
201
|
+
|
|
202
|
+
# Load metadata
|
|
203
|
+
metadata_path = path / "metadata.toml"
|
|
204
|
+
if not metadata_path.exists():
|
|
205
|
+
raise FileNotFoundError(f"metadata.toml not found in {path}")
|
|
206
|
+
metadata = PackageMetadata.from_toml(metadata_path)
|
|
207
|
+
|
|
208
|
+
# Load statute
|
|
209
|
+
statute_path = path / "statute.yh"
|
|
210
|
+
if not statute_path.exists():
|
|
211
|
+
raise FileNotFoundError(f"statute.yh not found in {path}")
|
|
212
|
+
statute_content = statute_path.read_text()
|
|
213
|
+
|
|
214
|
+
# Load tests (optional)
|
|
215
|
+
test_path = path / "test_statute.yh"
|
|
216
|
+
test_content = test_path.read_text() if test_path.exists() else None
|
|
217
|
+
|
|
218
|
+
# Load signature (optional)
|
|
219
|
+
sig_path = path / "signature"
|
|
220
|
+
signature = sig_path.read_bytes() if sig_path.exists() else None
|
|
221
|
+
|
|
222
|
+
return cls(
|
|
223
|
+
metadata=metadata,
|
|
224
|
+
statute_content=statute_content,
|
|
225
|
+
test_content=test_content,
|
|
226
|
+
signature=signature,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def from_yhpkg(cls, path: Path) -> "Package":
|
|
231
|
+
"""
|
|
232
|
+
Load package from .yhpkg file.
|
|
233
|
+
"""
|
|
234
|
+
import tempfile
|
|
235
|
+
|
|
236
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
237
|
+
tmpdir_path = Path(tmpdir)
|
|
238
|
+
|
|
239
|
+
# Extract tarball
|
|
240
|
+
with tarfile.open(path, "r:gz") as tar:
|
|
241
|
+
tar.extractall(tmpdir_path)
|
|
242
|
+
|
|
243
|
+
return cls.from_directory(tmpdir_path)
|
|
244
|
+
|
|
245
|
+
def to_yhpkg(self, output_path: Path) -> Path:
|
|
246
|
+
"""
|
|
247
|
+
Save package as .yhpkg file.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
output_path: Path to write .yhpkg file
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Path to created file
|
|
254
|
+
"""
|
|
255
|
+
import tempfile
|
|
256
|
+
|
|
257
|
+
output_path = Path(output_path)
|
|
258
|
+
if not output_path.suffix == ".yhpkg":
|
|
259
|
+
output_path = output_path.with_suffix(".yhpkg")
|
|
260
|
+
|
|
261
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
262
|
+
tmpdir_path = Path(tmpdir)
|
|
263
|
+
|
|
264
|
+
# Write statute
|
|
265
|
+
(tmpdir_path / "statute.yh").write_text(self.statute_content)
|
|
266
|
+
|
|
267
|
+
# Write tests if present
|
|
268
|
+
if self.test_content:
|
|
269
|
+
(tmpdir_path / "test_statute.yh").write_text(self.test_content)
|
|
270
|
+
|
|
271
|
+
# Write metadata as TOML
|
|
272
|
+
self._write_metadata_toml(tmpdir_path / "metadata.toml")
|
|
273
|
+
|
|
274
|
+
# Write signature if present
|
|
275
|
+
if self.signature:
|
|
276
|
+
(tmpdir_path / "signature").write_bytes(self.signature)
|
|
277
|
+
|
|
278
|
+
# Create gzipped tarball
|
|
279
|
+
with tarfile.open(output_path, "w:gz") as tar:
|
|
280
|
+
for item in tmpdir_path.iterdir():
|
|
281
|
+
tar.add(item, arcname=item.name)
|
|
282
|
+
|
|
283
|
+
return output_path
|
|
284
|
+
|
|
285
|
+
def _write_metadata_toml(self, path: Path) -> None:
|
|
286
|
+
"""Write metadata as TOML file."""
|
|
287
|
+
lines = [
|
|
288
|
+
f'section_number = "{self.metadata.section_number}"',
|
|
289
|
+
f'title = "{self.metadata.title}"',
|
|
290
|
+
f'jurisdiction = "{self.metadata.jurisdiction}"',
|
|
291
|
+
f'contributor = "{self.metadata.contributor}"',
|
|
292
|
+
f'version = "{self.metadata.version}"',
|
|
293
|
+
f'description = "{self.metadata.description}"',
|
|
294
|
+
f'license = "{self.metadata.license}"',
|
|
295
|
+
f'tags = {json.dumps(self.metadata.tags)}',
|
|
296
|
+
f'dependencies = {json.dumps(self.metadata.dependencies)}',
|
|
297
|
+
]
|
|
298
|
+
# Add deprecation section if deprecated
|
|
299
|
+
if self.metadata.deprecation.deprecated:
|
|
300
|
+
lines.append("")
|
|
301
|
+
lines.append("[deprecation]")
|
|
302
|
+
lines.append("deprecated = true")
|
|
303
|
+
if self.metadata.deprecation.reason:
|
|
304
|
+
lines.append(f'reason = "{self.metadata.deprecation.reason}"')
|
|
305
|
+
if self.metadata.deprecation.deprecated_since:
|
|
306
|
+
lines.append(f'deprecated_since = "{self.metadata.deprecation.deprecated_since}"')
|
|
307
|
+
if self.metadata.deprecation.replacement:
|
|
308
|
+
lines.append(f'replacement = "{self.metadata.deprecation.replacement}"')
|
|
309
|
+
if self.metadata.deprecation.removal_version:
|
|
310
|
+
lines.append(f'removal_version = "{self.metadata.deprecation.removal_version}"')
|
|
311
|
+
path.write_text("\n".join(lines))
|
|
312
|
+
|
|
313
|
+
def content_hash(self) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Calculate content hash for package.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
SHA-256 hash of statute content
|
|
319
|
+
"""
|
|
320
|
+
return hashlib.sha256(self.statute_content.encode()).hexdigest()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class PackageValidator:
|
|
324
|
+
"""
|
|
325
|
+
Validates Yuho statute packages.
|
|
326
|
+
|
|
327
|
+
Checks:
|
|
328
|
+
- Metadata completeness
|
|
329
|
+
- Statute parsability
|
|
330
|
+
- Test execution
|
|
331
|
+
- Signature verification (if provided)
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def __init__(self, strict: bool = True):
|
|
335
|
+
"""
|
|
336
|
+
Initialize validator.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
strict: If True, fail on warnings. If False, only fail on errors.
|
|
340
|
+
"""
|
|
341
|
+
self.strict = strict
|
|
342
|
+
|
|
343
|
+
def validate(self, package: Package) -> tuple[bool, List[str], List[str]]:
|
|
344
|
+
"""
|
|
345
|
+
Validate a package.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
package: Package to validate
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Tuple of (is_valid, errors, warnings)
|
|
352
|
+
"""
|
|
353
|
+
errors = []
|
|
354
|
+
warnings = []
|
|
355
|
+
|
|
356
|
+
# Validate metadata
|
|
357
|
+
meta_valid, meta_errors = package.metadata.is_valid()
|
|
358
|
+
if not meta_valid:
|
|
359
|
+
errors.extend(f"Metadata: {e}" for e in meta_errors)
|
|
360
|
+
|
|
361
|
+
# Check for deprecation
|
|
362
|
+
deprecation_warning = package.metadata.get_deprecation_warning()
|
|
363
|
+
if deprecation_warning:
|
|
364
|
+
warnings.append(deprecation_warning)
|
|
365
|
+
|
|
366
|
+
# Validate statute parsability
|
|
367
|
+
parse_errors = self._check_parsability(package.statute_content)
|
|
368
|
+
if parse_errors:
|
|
369
|
+
errors.extend(f"Parse: {e}" for e in parse_errors)
|
|
370
|
+
|
|
371
|
+
# Validate tests if present
|
|
372
|
+
if package.test_content:
|
|
373
|
+
test_errors = self._check_parsability(package.test_content)
|
|
374
|
+
if test_errors:
|
|
375
|
+
warnings.extend(f"Test parse: {e}" for e in test_errors)
|
|
376
|
+
else:
|
|
377
|
+
warnings.append("No test file provided")
|
|
378
|
+
|
|
379
|
+
# Check signature if present
|
|
380
|
+
if package.signature:
|
|
381
|
+
sig_valid = self._verify_signature(package)
|
|
382
|
+
if not sig_valid:
|
|
383
|
+
errors.append("Signature verification failed")
|
|
384
|
+
else:
|
|
385
|
+
warnings.append("No signature provided")
|
|
386
|
+
|
|
387
|
+
# Determine overall validity
|
|
388
|
+
is_valid = len(errors) == 0
|
|
389
|
+
if self.strict and warnings:
|
|
390
|
+
is_valid = False
|
|
391
|
+
|
|
392
|
+
return (is_valid, errors, warnings)
|
|
393
|
+
|
|
394
|
+
def _check_parsability(self, content: str) -> List[str]:
|
|
395
|
+
"""Check if content parses successfully."""
|
|
396
|
+
errors = []
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
from yuho.parser import Parser
|
|
400
|
+
parser = Parser()
|
|
401
|
+
result = parser.parse(content)
|
|
402
|
+
|
|
403
|
+
if result.errors:
|
|
404
|
+
errors.extend(str(e) for e in result.errors)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
errors.append(f"Parse error: {e}")
|
|
407
|
+
|
|
408
|
+
return errors
|
|
409
|
+
|
|
410
|
+
def _verify_signature(self, package: Package) -> bool:
|
|
411
|
+
"""Verify package signature."""
|
|
412
|
+
if not package.signature:
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
# In production, this would verify against contributor's public key
|
|
416
|
+
# For now, just check signature format
|
|
417
|
+
try:
|
|
418
|
+
# Ed25519 signatures are 64 bytes
|
|
419
|
+
return len(package.signature) == 64
|
|
420
|
+
except Exception:
|
|
421
|
+
return False
|