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,311 @@
1
+ """
2
+ Config command - Display and modify Yuho configuration.
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional, List, Dict, Any
8
+
9
+ import click
10
+
11
+ from yuho.config.loader import get_config, load_config, DEFAULT_CONFIG_PATH, create_default_config
12
+ from yuho.cli.error_formatter import Colors, colorize
13
+
14
+
15
+ class ConfigValidationError(Exception):
16
+ """Raised when configuration values are invalid."""
17
+ pass
18
+
19
+
20
+ def validate_config_value(section: str, key: str, value: str) -> Any:
21
+ """
22
+ Validate and convert a configuration value.
23
+
24
+ Args:
25
+ section: Config section (llm, transpile, lsp, mcp)
26
+ key: Configuration key
27
+ value: String value to validate
28
+
29
+ Returns:
30
+ Converted and validated value
31
+
32
+ Raises:
33
+ ConfigValidationError: If value is invalid
34
+ """
35
+ # Type mappings for validation
36
+ int_fields = {
37
+ "llm": ["ollama_port", "max_tokens"],
38
+ "transpile": [],
39
+ "lsp": [],
40
+ "mcp": ["port"],
41
+ }
42
+
43
+ float_fields = {
44
+ "llm": ["temperature"],
45
+ "transpile": [],
46
+ "lsp": [],
47
+ "mcp": [],
48
+ }
49
+
50
+ bool_fields = {
51
+ "llm": [],
52
+ "transpile": ["include_source_locations"],
53
+ "lsp": ["diagnostic_severity_error", "diagnostic_severity_warning",
54
+ "diagnostic_severity_info", "diagnostic_severity_hint"],
55
+ "mcp": [],
56
+ }
57
+
58
+ list_fields = {
59
+ "llm": ["fallback_providers"],
60
+ "transpile": [],
61
+ "lsp": ["completion_trigger_chars"],
62
+ "mcp": ["allowed_origins"],
63
+ }
64
+
65
+ # Enum validations
66
+ enum_fields = {
67
+ ("llm", "provider"): ["ollama", "huggingface", "openai", "anthropic"],
68
+ ("transpile", "default_target"): ["json", "jsonld", "english", "mermaid", "alloy", "latex"],
69
+ ("transpile", "latex_compiler"): ["pdflatex", "xelatex", "lualatex"],
70
+ }
71
+
72
+ # Range validations
73
+ range_fields = {
74
+ ("llm", "ollama_port"): (1, 65535),
75
+ ("llm", "max_tokens"): (1, 100000),
76
+ ("llm", "temperature"): (0.0, 2.0),
77
+ ("mcp", "port"): (1, 65535),
78
+ }
79
+
80
+ if section not in ["llm", "transpile", "lsp", "mcp"]:
81
+ raise ConfigValidationError(f"Unknown configuration section: '{section}'. Valid sections: llm, transpile, lsp, mcp")
82
+
83
+ # Check if key exists in section
84
+ valid_keys = {
85
+ "llm": ["provider", "model", "ollama_host", "ollama_port", "huggingface_cache",
86
+ "openai_api_key", "anthropic_api_key", "max_tokens", "temperature", "fallback_providers"],
87
+ "transpile": ["default_target", "latex_compiler", "output_dir", "include_source_locations"],
88
+ "lsp": ["diagnostic_severity_error", "diagnostic_severity_warning",
89
+ "diagnostic_severity_info", "diagnostic_severity_hint", "completion_trigger_chars"],
90
+ "mcp": ["host", "port", "allowed_origins", "auth_token"],
91
+ }
92
+
93
+ if key not in valid_keys.get(section, []):
94
+ raise ConfigValidationError(
95
+ f"Unknown key '{key}' in section [{section}]. Valid keys: {', '.join(valid_keys[section])}"
96
+ )
97
+
98
+ # Type conversion and validation
99
+ if key in int_fields.get(section, []):
100
+ try:
101
+ converted = int(value)
102
+ except ValueError:
103
+ raise ConfigValidationError(f"'{key}' must be an integer, got: '{value}'")
104
+
105
+ # Range check
106
+ if (section, key) in range_fields:
107
+ min_val, max_val = range_fields[(section, key)]
108
+ if not (min_val <= converted <= max_val):
109
+ raise ConfigValidationError(f"'{key}' must be between {min_val} and {max_val}, got: {converted}")
110
+
111
+ return converted
112
+
113
+ elif key in float_fields.get(section, []):
114
+ try:
115
+ converted = float(value)
116
+ except ValueError:
117
+ raise ConfigValidationError(f"'{key}' must be a number, got: '{value}'")
118
+
119
+ # Range check
120
+ if (section, key) in range_fields:
121
+ min_val, max_val = range_fields[(section, key)]
122
+ if not (min_val <= converted <= max_val):
123
+ raise ConfigValidationError(f"'{key}' must be between {min_val} and {max_val}, got: {converted}")
124
+
125
+ return converted
126
+
127
+ elif key in bool_fields.get(section, []):
128
+ lower = value.lower()
129
+ if lower in ("true", "1", "yes", "on"):
130
+ return True
131
+ elif lower in ("false", "0", "no", "off"):
132
+ return False
133
+ else:
134
+ raise ConfigValidationError(f"'{key}' must be a boolean (true/false), got: '{value}'")
135
+
136
+ elif key in list_fields.get(section, []):
137
+ # Parse comma-separated list
138
+ items = [item.strip() for item in value.split(",") if item.strip()]
139
+ if not items:
140
+ raise ConfigValidationError(f"'{key}' must be a non-empty list")
141
+ return items
142
+
143
+ else:
144
+ # String field - check enum if applicable
145
+ if (section, key) in enum_fields:
146
+ valid_values = enum_fields[(section, key)]
147
+ if value not in valid_values:
148
+ raise ConfigValidationError(
149
+ f"'{key}' must be one of: {', '.join(valid_values)}. Got: '{value}'"
150
+ )
151
+
152
+ return value
153
+
154
+
155
+ def run_config_show(section: Optional[str] = None, format: str = "toml", verbose: bool = False) -> None:
156
+ """
157
+ Display current configuration.
158
+
159
+ Args:
160
+ section: Optional section to show (llm, transpile, lsp, mcp)
161
+ format: Output format (toml, json)
162
+ verbose: Show additional info
163
+ """
164
+ config = get_config()
165
+ config_dict = config.to_dict()
166
+
167
+ # Filter to specific section if requested
168
+ if section:
169
+ if section not in config_dict:
170
+ click.echo(colorize(f"error: Unknown section '{section}'", Colors.RED), err=True)
171
+ click.echo(f"Valid sections: {', '.join(config_dict.keys())}", err=True)
172
+ sys.exit(1)
173
+ config_dict = {section: config_dict[section]}
174
+
175
+ if verbose:
176
+ click.echo(colorize(f"# Config loaded from: {DEFAULT_CONFIG_PATH}", Colors.DIM))
177
+ click.echo()
178
+
179
+ if format == "json":
180
+ import json
181
+ click.echo(json.dumps(config_dict, indent=2))
182
+ else:
183
+ # TOML format
184
+ for sect_name, sect_values in config_dict.items():
185
+ click.echo(colorize(f"[{sect_name}]", Colors.CYAN + Colors.BOLD))
186
+ for key, value in sect_values.items():
187
+ if isinstance(value, list):
188
+ value_str = f"[{', '.join(repr(v) for v in value)}]"
189
+ elif isinstance(value, str):
190
+ value_str = f'"{value}"'
191
+ elif isinstance(value, bool):
192
+ value_str = "true" if value else "false"
193
+ elif value is None:
194
+ value_str = colorize("# not set", Colors.DIM)
195
+ else:
196
+ value_str = str(value)
197
+ click.echo(f"{key} = {value_str}")
198
+ click.echo()
199
+
200
+
201
+ def run_config_set(key_path: str, value: str, verbose: bool = False) -> None:
202
+ """
203
+ Set a configuration value.
204
+
205
+ Args:
206
+ key_path: Key path like "llm.provider" or "mcp.port"
207
+ value: Value to set
208
+ verbose: Enable verbose output
209
+ """
210
+ # Parse key path
211
+ parts = key_path.split(".")
212
+ if len(parts) != 2:
213
+ click.echo(colorize(
214
+ f"error: Key must be in format 'section.key' (e.g., 'llm.provider')",
215
+ Colors.RED
216
+ ), err=True)
217
+ sys.exit(1)
218
+
219
+ section, key = parts
220
+
221
+ # Validate the value
222
+ try:
223
+ validated_value = validate_config_value(section, key, value)
224
+ except ConfigValidationError as e:
225
+ click.echo(colorize(f"error: {e}", Colors.RED), err=True)
226
+ sys.exit(1)
227
+
228
+ # Load or create config file
229
+ if not DEFAULT_CONFIG_PATH.exists():
230
+ if verbose:
231
+ click.echo(colorize(f"Creating config file: {DEFAULT_CONFIG_PATH}", Colors.YELLOW))
232
+ create_default_config()
233
+
234
+ # Read current config
235
+ try:
236
+ import tomllib
237
+ except ImportError:
238
+ try:
239
+ import tomli as tomllib
240
+ except ImportError:
241
+ click.echo(colorize("error: tomllib/tomli not available", Colors.RED), err=True)
242
+ sys.exit(1)
243
+
244
+ with open(DEFAULT_CONFIG_PATH, "rb") as f:
245
+ config_data = tomllib.load(f)
246
+
247
+ # Update value
248
+ if section not in config_data:
249
+ config_data[section] = {}
250
+ config_data[section][key] = validated_value
251
+
252
+ # Write back using tomlkit for formatting preservation
253
+ try:
254
+ import tomlkit
255
+ with open(DEFAULT_CONFIG_PATH, "r") as f:
256
+ doc = tomlkit.parse(f.read())
257
+
258
+ if section not in doc:
259
+ doc[section] = tomlkit.table()
260
+ doc[section][key] = validated_value
261
+
262
+ with open(DEFAULT_CONFIG_PATH, "w") as f:
263
+ f.write(tomlkit.dumps(doc))
264
+ except ImportError:
265
+ # Fall back to basic TOML writing
266
+ _write_config_basic(config_data)
267
+
268
+ if verbose:
269
+ click.echo(colorize(f"Updated {key_path} = {validated_value}", Colors.GREEN))
270
+ else:
271
+ click.echo(f"Set {key_path} = {validated_value}")
272
+
273
+
274
+ def run_config_init(force: bool = False, verbose: bool = False) -> None:
275
+ """
276
+ Create a default configuration file.
277
+
278
+ Args:
279
+ force: Overwrite existing file
280
+ verbose: Enable verbose output
281
+ """
282
+ if DEFAULT_CONFIG_PATH.exists() and not force:
283
+ click.echo(colorize(f"Config file already exists: {DEFAULT_CONFIG_PATH}", Colors.YELLOW))
284
+ click.echo("Use --force to overwrite.")
285
+ sys.exit(1)
286
+
287
+ path = create_default_config()
288
+ click.echo(colorize(f"Created config file: {path}", Colors.GREEN))
289
+
290
+
291
+ def _write_config_basic(config_data: Dict[str, Any]) -> None:
292
+ """Write config using basic TOML formatting."""
293
+ lines = []
294
+
295
+ for section, values in config_data.items():
296
+ lines.append(f"[{section}]")
297
+ for key, value in values.items():
298
+ if isinstance(value, list):
299
+ value_str = f"[{', '.join(repr(v) for v in value)}]"
300
+ elif isinstance(value, str):
301
+ value_str = f'"{value}"'
302
+ elif isinstance(value, bool):
303
+ value_str = "true" if value else "false"
304
+ elif value is None:
305
+ continue # Skip None values
306
+ else:
307
+ value_str = str(value)
308
+ lines.append(f"{key} = {value_str}")
309
+ lines.append("")
310
+
311
+ DEFAULT_CONFIG_PATH.write_text("\n".join(lines))
@@ -0,0 +1,122 @@
1
+ """
2
+ Contribute command - validate and package statutes for sharing.
3
+ """
4
+
5
+ import json
6
+ import sys
7
+ import tarfile
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+
13
+ from yuho.parser import Parser
14
+ from yuho.ast import ASTBuilder
15
+ from yuho.cli.error_formatter import Colors, colorize
16
+
17
+
18
+ def run_contribute(file: str, package: bool = False, output: Optional[str] = None, verbose: bool = False) -> None:
19
+ """
20
+ Validate a statute file for contribution.
21
+
22
+ Args:
23
+ file: Path to the .yh file
24
+ package: Create distributable package
25
+ output: Output path for package
26
+ verbose: Enable verbose output
27
+ """
28
+ file_path = Path(file)
29
+
30
+ if verbose:
31
+ click.echo(f"Validating {file_path}...")
32
+
33
+ # Parse and validate
34
+ parser = Parser()
35
+ try:
36
+ result = parser.parse_file(file_path)
37
+ except FileNotFoundError:
38
+ click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
39
+ sys.exit(1)
40
+
41
+ if result.errors:
42
+ click.echo(colorize(f"FAIL: Parse errors in {file}", Colors.RED))
43
+ for err in result.errors:
44
+ click.echo(f" {err.location}: {err.message}")
45
+ sys.exit(1)
46
+
47
+ builder = ASTBuilder(result.source, str(file_path))
48
+ ast = builder.build(result.root_node)
49
+
50
+ # Check for required metadata
51
+ if not ast.statutes:
52
+ click.echo(colorize("FAIL: No statute definitions found", Colors.RED))
53
+ sys.exit(1)
54
+
55
+ # Check for associated tests
56
+ test_paths = [
57
+ file_path.parent / f"test_{file_path.name}",
58
+ file_path.parent / "tests" / f"{file_path.stem}_test.yh",
59
+ ]
60
+ test_file = None
61
+ for tp in test_paths:
62
+ if tp.exists():
63
+ test_file = tp
64
+ break
65
+
66
+ if not test_file:
67
+ click.echo(colorize("WARNING: No test file found", Colors.YELLOW))
68
+ click.echo(f" Expected: test_{file_path.name} or tests/{file_path.stem}_test.yh")
69
+
70
+ # Check for metadata.toml
71
+ metadata_path = file_path.parent / "metadata.toml"
72
+ has_metadata = metadata_path.exists()
73
+ if not has_metadata:
74
+ click.echo(colorize("WARNING: No metadata.toml found", Colors.YELLOW))
75
+
76
+ # Summary
77
+ click.echo(colorize(f"OK: {file_path} is valid for contribution", Colors.CYAN + Colors.BOLD))
78
+ click.echo(f" Statutes: {len(ast.statutes)}")
79
+ for statute in ast.statutes:
80
+ title = statute.title.value if statute.title else "(untitled)"
81
+ click.echo(f" - Section {statute.section_number}: {title}")
82
+
83
+ if test_file:
84
+ click.echo(f" Tests: {test_file}")
85
+ if has_metadata:
86
+ click.echo(f" Metadata: {metadata_path}")
87
+
88
+ # Package if requested
89
+ if package:
90
+ _create_package(file_path, test_file, metadata_path if has_metadata else None, output, verbose)
91
+
92
+
93
+ def _create_package(
94
+ statute_file: Path,
95
+ test_file: Optional[Path],
96
+ metadata_file: Optional[Path],
97
+ output: Optional[str],
98
+ verbose: bool
99
+ ) -> None:
100
+ """Create a .yhpkg archive."""
101
+ # Determine output path
102
+ if output:
103
+ pkg_path = Path(output)
104
+ else:
105
+ pkg_path = statute_file.parent / f"{statute_file.stem}.yhpkg"
106
+
107
+ if verbose:
108
+ click.echo(f"Creating package: {pkg_path}")
109
+
110
+ with tarfile.open(pkg_path, "w:gz") as tar:
111
+ # Add statute file
112
+ tar.add(statute_file, arcname=statute_file.name)
113
+
114
+ # Add test file if exists
115
+ if test_file:
116
+ tar.add(test_file, arcname=f"tests/{test_file.name}")
117
+
118
+ # Add metadata if exists
119
+ if metadata_file:
120
+ tar.add(metadata_file, arcname="metadata.toml")
121
+
122
+ click.echo(colorize(f"Package created: {pkg_path}", Colors.CYAN + Colors.BOLD))