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/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]
|