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
yuho/library/index.py ADDED
@@ -0,0 +1,328 @@
1
+ """
2
+ Library index for Yuho statute packages.
3
+
4
+ Provides search and discovery of statute packages by section number,
5
+ title, jurisdiction, and keywords.
6
+ """
7
+
8
+ from typing import Optional, List, Dict, Any
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ import json
12
+ import logging
13
+
14
+ from yuho.library.package import PackageMetadata
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Default library paths
20
+ DEFAULT_LIBRARY_INDEX = Path.home() / ".yuho" / "library" / "index.json"
21
+ DEFAULT_LIBRARY_DIR = Path.home() / ".yuho" / "library" / "packages"
22
+
23
+
24
+ @dataclass
25
+ class IndexEntry:
26
+ """An entry in the library index."""
27
+ section_number: str
28
+ title: str
29
+ jurisdiction: str
30
+ contributor: str
31
+ version: str
32
+ description: str
33
+ tags: List[str]
34
+ package_path: str # Relative path within library
35
+ content_hash: str
36
+
37
+ @classmethod
38
+ def from_metadata(
39
+ cls, metadata: PackageMetadata, package_path: str, content_hash: str
40
+ ) -> "IndexEntry":
41
+ """Create entry from package metadata."""
42
+ return cls(
43
+ section_number=metadata.section_number,
44
+ title=metadata.title,
45
+ jurisdiction=metadata.jurisdiction,
46
+ contributor=metadata.contributor,
47
+ version=metadata.version,
48
+ description=metadata.description,
49
+ tags=metadata.tags,
50
+ package_path=package_path,
51
+ content_hash=content_hash,
52
+ )
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ """Convert to dictionary."""
56
+ return {
57
+ "section_number": self.section_number,
58
+ "title": self.title,
59
+ "jurisdiction": self.jurisdiction,
60
+ "contributor": self.contributor,
61
+ "version": self.version,
62
+ "description": self.description,
63
+ "tags": self.tags,
64
+ "package_path": self.package_path,
65
+ "content_hash": self.content_hash,
66
+ }
67
+
68
+ @classmethod
69
+ def from_dict(cls, data: Dict[str, Any]) -> "IndexEntry":
70
+ """Create from dictionary."""
71
+ return cls(
72
+ section_number=data["section_number"],
73
+ title=data["title"],
74
+ jurisdiction=data["jurisdiction"],
75
+ contributor=data["contributor"],
76
+ version=data["version"],
77
+ description=data.get("description", ""),
78
+ tags=data.get("tags", []),
79
+ package_path=data["package_path"],
80
+ content_hash=data.get("content_hash", ""),
81
+ )
82
+
83
+ def matches(
84
+ self,
85
+ section: Optional[str] = None,
86
+ keyword: Optional[str] = None,
87
+ jurisdiction: Optional[str] = None,
88
+ tags: Optional[List[str]] = None,
89
+ ) -> bool:
90
+ """Check if entry matches search criteria."""
91
+ if section and section.lower() not in self.section_number.lower():
92
+ return False
93
+
94
+ if jurisdiction and jurisdiction.lower() not in self.jurisdiction.lower():
95
+ return False
96
+
97
+ if keyword:
98
+ keyword_lower = keyword.lower()
99
+ searchable = f"{self.title} {self.description} {' '.join(self.tags)}".lower()
100
+ if keyword_lower not in searchable:
101
+ return False
102
+
103
+ if tags:
104
+ # Entry must have at least one of the specified tags
105
+ entry_tags_lower = [t.lower() for t in self.tags]
106
+ matching_tags = any(t.lower() in entry_tags_lower for t in tags)
107
+ if not matching_tags:
108
+ return False
109
+
110
+ return True
111
+
112
+
113
+ class LibraryIndex:
114
+ """
115
+ Index of installed statute packages.
116
+
117
+ Provides efficient lookup by section number, jurisdiction,
118
+ and keyword search.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ index_path: Optional[Path] = None,
124
+ library_dir: Optional[Path] = None,
125
+ ):
126
+ """
127
+ Initialize the library index.
128
+
129
+ Args:
130
+ index_path: Path to index JSON file
131
+ library_dir: Path to library directory
132
+ """
133
+ self.index_path = index_path or DEFAULT_LIBRARY_INDEX
134
+ self.library_dir = library_dir or DEFAULT_LIBRARY_DIR
135
+ self._entries: Dict[str, IndexEntry] = {}
136
+ self._load()
137
+
138
+ def _load(self) -> None:
139
+ """Load index from disk."""
140
+ if not self.index_path.exists():
141
+ logger.debug(f"No index found at {self.index_path}")
142
+ return
143
+
144
+ try:
145
+ with open(self.index_path) as f:
146
+ data = json.load(f)
147
+
148
+ for entry_data in data.get("entries", []):
149
+ entry = IndexEntry.from_dict(entry_data)
150
+ self._entries[entry.section_number] = entry
151
+
152
+ logger.debug(f"Loaded {len(self._entries)} index entries")
153
+ except Exception as e:
154
+ logger.warning(f"Failed to load index: {e}")
155
+
156
+ def _save(self) -> None:
157
+ """Save index to disk."""
158
+ self.index_path.parent.mkdir(parents=True, exist_ok=True)
159
+
160
+ data = {
161
+ "version": "1.0",
162
+ "entries": [e.to_dict() for e in self._entries.values()],
163
+ }
164
+
165
+ with open(self.index_path, "w") as f:
166
+ json.dump(data, f, indent=2)
167
+
168
+ def add(self, entry: IndexEntry) -> None:
169
+ """Add or update an entry in the index."""
170
+ self._entries[entry.section_number] = entry
171
+ self._save()
172
+
173
+ def remove(self, section_number: str) -> bool:
174
+ """
175
+ Remove an entry from the index.
176
+
177
+ Returns:
178
+ True if entry was removed, False if not found
179
+ """
180
+ if section_number in self._entries:
181
+ del self._entries[section_number]
182
+ self._save()
183
+ return True
184
+ return False
185
+
186
+ def get(self, section_number: str) -> Optional[IndexEntry]:
187
+ """Get an entry by section number."""
188
+ return self._entries.get(section_number)
189
+
190
+ def search(
191
+ self,
192
+ section: Optional[str] = None,
193
+ keyword: Optional[str] = None,
194
+ jurisdiction: Optional[str] = None,
195
+ tags: Optional[List[str]] = None,
196
+ limit: int = 50,
197
+ ) -> List[IndexEntry]:
198
+ """
199
+ Search the library index.
200
+
201
+ Args:
202
+ section: Section number pattern to match
203
+ keyword: Keyword to search in title/description/tags
204
+ jurisdiction: Jurisdiction to filter by
205
+ tags: Tags to filter by (matches if entry has any)
206
+ limit: Maximum results to return
207
+
208
+ Returns:
209
+ List of matching entries
210
+ """
211
+ results = []
212
+
213
+ for entry in self._entries.values():
214
+ if entry.matches(section, keyword, jurisdiction, tags):
215
+ results.append(entry)
216
+ if len(results) >= limit:
217
+ break
218
+
219
+ # Sort by section number
220
+ results.sort(key=lambda e: e.section_number)
221
+
222
+ return results
223
+
224
+ def list_all(self) -> List[IndexEntry]:
225
+ """List all entries in the index."""
226
+ return sorted(self._entries.values(), key=lambda e: e.section_number)
227
+
228
+ def count(self) -> int:
229
+ """Get total number of indexed packages."""
230
+ return len(self._entries)
231
+
232
+ def detect_conflicts(self) -> List[Dict[str, Any]]:
233
+ """
234
+ Detect conflicts where multiple packages define the same section.
235
+
236
+ Returns:
237
+ List of conflicts, each with 'section_number', 'packages' list
238
+ """
239
+ from collections import defaultdict
240
+
241
+ # This implementation tracks if multiple sources provide same section
242
+ # In current implementation, each section maps to one entry
243
+ # Conflicts occur when trying to install a package with existing section
244
+ conflicts = []
245
+
246
+ # For enhanced conflict detection, check package_path uniqueness
247
+ path_to_sections: Dict[str, List[str]] = defaultdict(list)
248
+
249
+ for entry in self._entries.values():
250
+ path_to_sections[entry.package_path].append(entry.section_number)
251
+
252
+ # Find duplicate paths (shouldn't happen in normal operation)
253
+ for path, sections in path_to_sections.items():
254
+ if len(sections) > 1:
255
+ conflicts.append({
256
+ "type": "duplicate_path",
257
+ "package_path": path,
258
+ "sections": sections,
259
+ "message": f"Package {path} provides multiple sections",
260
+ })
261
+
262
+ return conflicts
263
+
264
+ def check_section_conflict(self, section_number: str) -> Optional[IndexEntry]:
265
+ """
266
+ Check if installing a section would conflict with existing package.
267
+
268
+ Args:
269
+ section_number: Section number to check
270
+
271
+ Returns:
272
+ Existing conflicting entry, or None if no conflict
273
+ """
274
+ return self._entries.get(section_number)
275
+
276
+ def rebuild(self) -> int:
277
+ """
278
+ Rebuild index from installed packages.
279
+
280
+ Returns:
281
+ Number of packages indexed
282
+ """
283
+ self._entries.clear()
284
+
285
+ if not self.library_dir.exists():
286
+ return 0
287
+
288
+ from yuho.library.package import Package
289
+
290
+ count = 0
291
+ for pkg_path in self.library_dir.glob("*.yhpkg"):
292
+ try:
293
+ pkg = Package.from_yhpkg(pkg_path)
294
+ entry = IndexEntry.from_metadata(
295
+ pkg.metadata,
296
+ pkg_path.name,
297
+ pkg.content_hash(),
298
+ )
299
+ self._entries[entry.section_number] = entry
300
+ count += 1
301
+ except Exception as e:
302
+ logger.warning(f"Failed to index {pkg_path}: {e}")
303
+
304
+ self._save()
305
+ return count
306
+
307
+
308
+ def search_library(
309
+ section: Optional[str] = None,
310
+ keyword: Optional[str] = None,
311
+ jurisdiction: Optional[str] = None,
312
+ tags: Optional[List[str]] = None,
313
+ ) -> List[Dict[str, Any]]:
314
+ """
315
+ Convenience function to search the library.
316
+
317
+ Args:
318
+ section: Section number pattern
319
+ keyword: Search keyword
320
+ jurisdiction: Jurisdiction filter
321
+ tags: Tags to filter by
322
+
323
+ Returns:
324
+ List of matching packages as dictionaries
325
+ """
326
+ index = LibraryIndex()
327
+ results = index.search(section, keyword, jurisdiction, tags)
328
+ return [r.to_dict() for r in results]