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.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
@@ -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