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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph command - visualize statute dependencies.
|
|
3
|
+
|
|
4
|
+
Generates dependency graphs in DOT or Mermaid format showing:
|
|
5
|
+
- Statute cross-references
|
|
6
|
+
- Import relationships
|
|
7
|
+
- Type dependencies
|
|
8
|
+
- Function call graphs
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, List, Set, Dict, Tuple
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum, auto
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from yuho.parser import Parser
|
|
20
|
+
from yuho.ast import ASTBuilder
|
|
21
|
+
from yuho.ast.nodes import (
|
|
22
|
+
ModuleNode, StatuteNode, ImportNode, IdentifierNode,
|
|
23
|
+
FunctionCallNode, FieldAccessNode
|
|
24
|
+
)
|
|
25
|
+
from yuho.ast.visitor import Visitor
|
|
26
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphFormat(Enum):
|
|
30
|
+
"""Supported graph output formats."""
|
|
31
|
+
DOT = "dot"
|
|
32
|
+
MERMAID = "mermaid"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GraphNode:
|
|
37
|
+
"""A node in the dependency graph."""
|
|
38
|
+
id: str
|
|
39
|
+
label: str
|
|
40
|
+
node_type: str # "statute", "import", "type", "function"
|
|
41
|
+
|
|
42
|
+
def __hash__(self):
|
|
43
|
+
return hash(self.id)
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other):
|
|
46
|
+
return isinstance(other, GraphNode) and self.id == other.id
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class GraphEdge:
|
|
51
|
+
"""An edge in the dependency graph."""
|
|
52
|
+
source: str
|
|
53
|
+
target: str
|
|
54
|
+
edge_type: str # "references", "imports", "calls", "uses"
|
|
55
|
+
label: str = ""
|
|
56
|
+
|
|
57
|
+
def __hash__(self):
|
|
58
|
+
return hash((self.source, self.target, self.edge_type))
|
|
59
|
+
|
|
60
|
+
def __eq__(self, other):
|
|
61
|
+
return (isinstance(other, GraphEdge) and
|
|
62
|
+
self.source == other.source and
|
|
63
|
+
self.target == other.target and
|
|
64
|
+
self.edge_type == other.edge_type)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class DependencyGraph:
|
|
69
|
+
"""Represents the full dependency graph."""
|
|
70
|
+
nodes: Set[GraphNode] = field(default_factory=set)
|
|
71
|
+
edges: Set[GraphEdge] = field(default_factory=set)
|
|
72
|
+
|
|
73
|
+
def add_node(self, node: GraphNode) -> None:
|
|
74
|
+
self.nodes.add(node)
|
|
75
|
+
|
|
76
|
+
def add_edge(self, edge: GraphEdge) -> None:
|
|
77
|
+
self.edges.add(edge)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DependencyExtractor(Visitor):
|
|
81
|
+
"""
|
|
82
|
+
Extract dependencies from a Yuho AST.
|
|
83
|
+
|
|
84
|
+
Identifies:
|
|
85
|
+
- Statute cross-references (via section numbers)
|
|
86
|
+
- Import statements
|
|
87
|
+
- Type usages
|
|
88
|
+
- Function calls
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.graph = DependencyGraph()
|
|
93
|
+
self._current_context: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
def extract(self, ast: ModuleNode, filename: str = "main") -> DependencyGraph:
|
|
96
|
+
"""Extract dependency graph from AST."""
|
|
97
|
+
self.graph = DependencyGraph()
|
|
98
|
+
self._current_context = None
|
|
99
|
+
|
|
100
|
+
# Add module node
|
|
101
|
+
module_id = f"module:{filename}"
|
|
102
|
+
self.graph.add_node(GraphNode(
|
|
103
|
+
id=module_id,
|
|
104
|
+
label=filename,
|
|
105
|
+
node_type="module"
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
# Process imports
|
|
109
|
+
for imp in ast.imports:
|
|
110
|
+
self._process_import(module_id, imp)
|
|
111
|
+
|
|
112
|
+
# Process type definitions
|
|
113
|
+
for type_def in ast.type_defs:
|
|
114
|
+
type_id = f"type:{type_def.name}"
|
|
115
|
+
self.graph.add_node(GraphNode(
|
|
116
|
+
id=type_id,
|
|
117
|
+
label=type_def.name,
|
|
118
|
+
node_type="type"
|
|
119
|
+
))
|
|
120
|
+
self.graph.add_edge(GraphEdge(
|
|
121
|
+
source=module_id,
|
|
122
|
+
target=type_id,
|
|
123
|
+
edge_type="defines"
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# Process functions
|
|
127
|
+
for func in ast.function_defs:
|
|
128
|
+
func_id = f"function:{func.name}"
|
|
129
|
+
self.graph.add_node(GraphNode(
|
|
130
|
+
id=func_id,
|
|
131
|
+
label=func.name,
|
|
132
|
+
node_type="function"
|
|
133
|
+
))
|
|
134
|
+
self.graph.add_edge(GraphEdge(
|
|
135
|
+
source=module_id,
|
|
136
|
+
target=func_id,
|
|
137
|
+
edge_type="defines"
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
# Process statutes
|
|
141
|
+
for statute in ast.statutes:
|
|
142
|
+
self._process_statute(module_id, statute)
|
|
143
|
+
|
|
144
|
+
return self.graph
|
|
145
|
+
|
|
146
|
+
def _process_import(self, module_id: str, imp: ImportNode) -> None:
|
|
147
|
+
"""Process import statement."""
|
|
148
|
+
import_id = f"import:{imp.path}"
|
|
149
|
+
self.graph.add_node(GraphNode(
|
|
150
|
+
id=import_id,
|
|
151
|
+
label=imp.path,
|
|
152
|
+
node_type="import"
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
if imp.is_wildcard:
|
|
156
|
+
label = "*"
|
|
157
|
+
elif imp.imported_names:
|
|
158
|
+
label = ", ".join(imp.imported_names[:3])
|
|
159
|
+
if len(imp.imported_names) > 3:
|
|
160
|
+
label += "..."
|
|
161
|
+
else:
|
|
162
|
+
label = ""
|
|
163
|
+
|
|
164
|
+
self.graph.add_edge(GraphEdge(
|
|
165
|
+
source=module_id,
|
|
166
|
+
target=import_id,
|
|
167
|
+
edge_type="imports",
|
|
168
|
+
label=label
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
def _process_statute(self, module_id: str, statute: StatuteNode) -> None:
|
|
172
|
+
"""Process statute node."""
|
|
173
|
+
statute_id = f"statute:{statute.section_number}"
|
|
174
|
+
title = statute.title.value if statute.title else f"Section {statute.section_number}"
|
|
175
|
+
|
|
176
|
+
self.graph.add_node(GraphNode(
|
|
177
|
+
id=statute_id,
|
|
178
|
+
label=f"S.{statute.section_number}\\n{title[:30]}",
|
|
179
|
+
node_type="statute"
|
|
180
|
+
))
|
|
181
|
+
|
|
182
|
+
self.graph.add_edge(GraphEdge(
|
|
183
|
+
source=module_id,
|
|
184
|
+
target=statute_id,
|
|
185
|
+
edge_type="contains"
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
# Look for cross-references in definitions and elements
|
|
189
|
+
self._current_context = statute_id
|
|
190
|
+
|
|
191
|
+
# Check definitions for section references
|
|
192
|
+
for defn in statute.definitions:
|
|
193
|
+
self._extract_section_refs(statute_id, defn.definition.value)
|
|
194
|
+
|
|
195
|
+
# Check illustrations
|
|
196
|
+
for illus in statute.illustrations:
|
|
197
|
+
self._extract_section_refs(statute_id, illus.description.value)
|
|
198
|
+
|
|
199
|
+
def _extract_section_refs(self, source_id: str, text: str) -> None:
|
|
200
|
+
"""Extract section number references from text."""
|
|
201
|
+
import re
|
|
202
|
+
|
|
203
|
+
# Patterns for section references
|
|
204
|
+
patterns = [
|
|
205
|
+
r'[Ss]ection\s+(\d+[A-Za-z]*)',
|
|
206
|
+
r'[Ss]\.\s*(\d+[A-Za-z]*)',
|
|
207
|
+
r'§\s*(\d+[A-Za-z]*)',
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
for pattern in patterns:
|
|
211
|
+
for match in re.finditer(pattern, text):
|
|
212
|
+
ref_section = match.group(1)
|
|
213
|
+
ref_id = f"statute:{ref_section}"
|
|
214
|
+
|
|
215
|
+
# Add target node if not exists
|
|
216
|
+
self.graph.add_node(GraphNode(
|
|
217
|
+
id=ref_id,
|
|
218
|
+
label=f"S.{ref_section}",
|
|
219
|
+
node_type="statute"
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
# Add reference edge
|
|
223
|
+
self.graph.add_edge(GraphEdge(
|
|
224
|
+
source=source_id,
|
|
225
|
+
target=ref_id,
|
|
226
|
+
edge_type="references"
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def generate_dot(graph: DependencyGraph, title: str = "Yuho Dependencies") -> str:
|
|
231
|
+
"""
|
|
232
|
+
Generate DOT format graph.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
graph: The dependency graph
|
|
236
|
+
title: Graph title
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
DOT format string
|
|
240
|
+
"""
|
|
241
|
+
lines = [
|
|
242
|
+
f'digraph "{title}" {{',
|
|
243
|
+
' rankdir=TB;',
|
|
244
|
+
' node [fontname="Helvetica", fontsize=10];',
|
|
245
|
+
' edge [fontname="Helvetica", fontsize=8];',
|
|
246
|
+
'',
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
# Node styles by type
|
|
250
|
+
node_styles = {
|
|
251
|
+
"module": 'shape=box, style=filled, fillcolor="#E8F4F8"',
|
|
252
|
+
"statute": 'shape=box, style="filled,rounded", fillcolor="#FFF3CD"',
|
|
253
|
+
"import": 'shape=ellipse, style=filled, fillcolor="#D4EDDA"',
|
|
254
|
+
"type": 'shape=hexagon, style=filled, fillcolor="#F8D7DA"',
|
|
255
|
+
"function": 'shape=ellipse, style=filled, fillcolor="#CCE5FF"',
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Add nodes
|
|
259
|
+
for node in sorted(graph.nodes, key=lambda n: n.id):
|
|
260
|
+
style = node_styles.get(node.node_type, '')
|
|
261
|
+
safe_label = node.label.replace('"', '\\"')
|
|
262
|
+
lines.append(f' "{node.id}" [label="{safe_label}", {style}];')
|
|
263
|
+
|
|
264
|
+
lines.append('')
|
|
265
|
+
|
|
266
|
+
# Edge styles by type
|
|
267
|
+
edge_styles = {
|
|
268
|
+
"imports": 'style=dashed, color="#28a745"',
|
|
269
|
+
"references": 'style=solid, color="#dc3545"',
|
|
270
|
+
"defines": 'style=solid, color="#6c757d"',
|
|
271
|
+
"contains": 'style=dotted, color="#6c757d"',
|
|
272
|
+
"calls": 'style=solid, color="#007bff"',
|
|
273
|
+
"uses": 'style=dashed, color="#17a2b8"',
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Add edges
|
|
277
|
+
for edge in sorted(graph.edges, key=lambda e: (e.source, e.target)):
|
|
278
|
+
style = edge_styles.get(edge.edge_type, '')
|
|
279
|
+
label_part = f', label="{edge.label}"' if edge.label else ''
|
|
280
|
+
lines.append(f' "{edge.source}" -> "{edge.target}" [{style}{label_part}];')
|
|
281
|
+
|
|
282
|
+
lines.append('}')
|
|
283
|
+
return '\n'.join(lines)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def generate_mermaid(graph: DependencyGraph, title: str = "Yuho Dependencies") -> str:
|
|
287
|
+
"""
|
|
288
|
+
Generate Mermaid format graph.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
graph: The dependency graph
|
|
292
|
+
title: Graph title
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Mermaid format string
|
|
296
|
+
"""
|
|
297
|
+
lines = [
|
|
298
|
+
'```mermaid',
|
|
299
|
+
'flowchart TB',
|
|
300
|
+
f' %% {title}',
|
|
301
|
+
'',
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
# Node shapes by type
|
|
305
|
+
def node_shape(node: GraphNode) -> str:
|
|
306
|
+
safe_label = node.label.replace('"', "'").replace('\n', ' ')
|
|
307
|
+
shapes = {
|
|
308
|
+
"module": f'["{safe_label}"]',
|
|
309
|
+
"statute": f'("{safe_label}")',
|
|
310
|
+
"import": f'[["{safe_label}"]]',
|
|
311
|
+
"type": f'{{{{{safe_label}}}}}',
|
|
312
|
+
"function": f'(["{safe_label}"])',
|
|
313
|
+
}
|
|
314
|
+
return shapes.get(node.node_type, f'["{safe_label}"]')
|
|
315
|
+
|
|
316
|
+
# Sanitize ID for Mermaid
|
|
317
|
+
def safe_id(id: str) -> str:
|
|
318
|
+
return id.replace(":", "_").replace(".", "_").replace("/", "_")
|
|
319
|
+
|
|
320
|
+
# Add nodes
|
|
321
|
+
for node in sorted(graph.nodes, key=lambda n: n.id):
|
|
322
|
+
sid = safe_id(node.id)
|
|
323
|
+
lines.append(f' {sid}{node_shape(node)}')
|
|
324
|
+
|
|
325
|
+
lines.append('')
|
|
326
|
+
|
|
327
|
+
# Edge styles
|
|
328
|
+
edge_arrows = {
|
|
329
|
+
"imports": "-.->",
|
|
330
|
+
"references": "-->",
|
|
331
|
+
"defines": "-->",
|
|
332
|
+
"contains": "-.->",
|
|
333
|
+
"calls": "-->",
|
|
334
|
+
"uses": "-.->",
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Add edges
|
|
338
|
+
for edge in sorted(graph.edges, key=lambda e: (e.source, e.target)):
|
|
339
|
+
src = safe_id(edge.source)
|
|
340
|
+
tgt = safe_id(edge.target)
|
|
341
|
+
arrow = edge_arrows.get(edge.edge_type, "-->")
|
|
342
|
+
if edge.label:
|
|
343
|
+
lines.append(f' {src} {arrow}|{edge.label}| {tgt}')
|
|
344
|
+
else:
|
|
345
|
+
lines.append(f' {src} {arrow} {tgt}')
|
|
346
|
+
|
|
347
|
+
lines.append('```')
|
|
348
|
+
return '\n'.join(lines)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run_graph(
|
|
352
|
+
file: str,
|
|
353
|
+
format: str = "mermaid",
|
|
354
|
+
output: Optional[str] = None,
|
|
355
|
+
verbose: bool = False,
|
|
356
|
+
color: bool = True,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Generate dependency graph for a Yuho file.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
file: Path to the .yh file
|
|
363
|
+
format: Output format (dot or mermaid)
|
|
364
|
+
output: Output file path (stdout if None)
|
|
365
|
+
verbose: Enable verbose output
|
|
366
|
+
color: Use colored output
|
|
367
|
+
"""
|
|
368
|
+
path = Path(file)
|
|
369
|
+
|
|
370
|
+
if not path.exists():
|
|
371
|
+
click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
|
|
374
|
+
# Parse file
|
|
375
|
+
parser = Parser()
|
|
376
|
+
result = parser.parse_file(path)
|
|
377
|
+
|
|
378
|
+
if result.errors:
|
|
379
|
+
click.echo(colorize(f"error: Parse errors in {file}:", Colors.RED), err=True)
|
|
380
|
+
for err in result.errors[:3]:
|
|
381
|
+
click.echo(f" {err.message}", err=True)
|
|
382
|
+
sys.exit(1)
|
|
383
|
+
|
|
384
|
+
# Build AST
|
|
385
|
+
builder = ASTBuilder()
|
|
386
|
+
ast = builder.build(result.tree)
|
|
387
|
+
|
|
388
|
+
# Extract dependencies
|
|
389
|
+
extractor = DependencyExtractor()
|
|
390
|
+
graph = extractor.extract(ast, path.stem)
|
|
391
|
+
|
|
392
|
+
if verbose:
|
|
393
|
+
click.echo(f"Extracted {len(graph.nodes)} nodes and {len(graph.edges)} edges")
|
|
394
|
+
|
|
395
|
+
# Generate output
|
|
396
|
+
graph_format = GraphFormat(format.lower())
|
|
397
|
+
|
|
398
|
+
if graph_format == GraphFormat.DOT:
|
|
399
|
+
output_str = generate_dot(graph, title=f"Dependencies: {path.name}")
|
|
400
|
+
else:
|
|
401
|
+
output_str = generate_mermaid(graph, title=f"Dependencies: {path.name}")
|
|
402
|
+
|
|
403
|
+
# Write output
|
|
404
|
+
if output:
|
|
405
|
+
out_path = Path(output)
|
|
406
|
+
out_path.write_text(output_str)
|
|
407
|
+
if verbose:
|
|
408
|
+
click.echo(f"Wrote graph to {out_path}")
|
|
409
|
+
else:
|
|
410
|
+
print(output_str)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Init command - scaffold new Yuho project.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TEMPLATE_STATUTE = '''// {name} - Yuho statute definition
|
|
15
|
+
// Section: S{section}
|
|
16
|
+
|
|
17
|
+
statute {section} "{name}" {{
|
|
18
|
+
definitions {{
|
|
19
|
+
// Define key terms here
|
|
20
|
+
// term := "definition";
|
|
21
|
+
}}
|
|
22
|
+
|
|
23
|
+
elements {{
|
|
24
|
+
// Define the elements of the offense
|
|
25
|
+
// actus_reus physical_act := "description";
|
|
26
|
+
// mens_rea mental_state := "description";
|
|
27
|
+
}}
|
|
28
|
+
|
|
29
|
+
penalty {{
|
|
30
|
+
// imprisonment := 0 years .. 1 year;
|
|
31
|
+
// fine := $0 .. $10,000;
|
|
32
|
+
}}
|
|
33
|
+
|
|
34
|
+
illustration A {{
|
|
35
|
+
"Example scenario illustrating the statute."
|
|
36
|
+
}}
|
|
37
|
+
}}
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
TEMPLATE_METADATA = '''# Metadata for {name}
|
|
41
|
+
|
|
42
|
+
[statute]
|
|
43
|
+
section_number = "{section}"
|
|
44
|
+
title = "{name}"
|
|
45
|
+
jurisdiction = "SG" # Singapore
|
|
46
|
+
|
|
47
|
+
[contributor]
|
|
48
|
+
name = ""
|
|
49
|
+
email = ""
|
|
50
|
+
|
|
51
|
+
[version]
|
|
52
|
+
current = "1.0.0"
|
|
53
|
+
'''
|
|
54
|
+
|
|
55
|
+
TEMPLATE_TEST = '''// Tests for {name}
|
|
56
|
+
// Run with: yuho test {filename}
|
|
57
|
+
|
|
58
|
+
// Test case 1: Basic scenario
|
|
59
|
+
// TODO: Add test cases
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_init(name: Optional[str] = None, directory: Optional[str] = None, verbose: bool = False) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Initialize a new Yuho statute project.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: Project name
|
|
69
|
+
directory: Directory to create project in
|
|
70
|
+
verbose: Enable verbose output
|
|
71
|
+
"""
|
|
72
|
+
# Get project name
|
|
73
|
+
if not name:
|
|
74
|
+
name = click.prompt("Statute name", default="MyStatute")
|
|
75
|
+
|
|
76
|
+
# Clean name for filenames
|
|
77
|
+
safe_name = "".join(c if c.isalnum() else "_" for c in name)
|
|
78
|
+
section = click.prompt("Section number", default="000")
|
|
79
|
+
|
|
80
|
+
# Determine directory
|
|
81
|
+
if directory:
|
|
82
|
+
project_dir = Path(directory)
|
|
83
|
+
else:
|
|
84
|
+
project_dir = Path.cwd() / safe_name.lower()
|
|
85
|
+
|
|
86
|
+
# Check if exists
|
|
87
|
+
if project_dir.exists():
|
|
88
|
+
if not click.confirm(f"Directory {project_dir} exists. Continue?"):
|
|
89
|
+
sys.exit(0)
|
|
90
|
+
else:
|
|
91
|
+
project_dir.mkdir(parents=True)
|
|
92
|
+
|
|
93
|
+
# Create files
|
|
94
|
+
statute_file = project_dir / f"{safe_name.lower()}.yh"
|
|
95
|
+
metadata_file = project_dir / "metadata.toml"
|
|
96
|
+
test_dir = project_dir / "tests"
|
|
97
|
+
test_file = test_dir / f"test_{safe_name.lower()}.yh"
|
|
98
|
+
|
|
99
|
+
# Write statute template
|
|
100
|
+
statute_content = TEMPLATE_STATUTE.format(name=name, section=section)
|
|
101
|
+
statute_file.write_text(statute_content, encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
# Write metadata template
|
|
104
|
+
metadata_content = TEMPLATE_METADATA.format(name=name, section=section)
|
|
105
|
+
metadata_file.write_text(metadata_content, encoding="utf-8")
|
|
106
|
+
|
|
107
|
+
# Write test template
|
|
108
|
+
test_dir.mkdir(exist_ok=True)
|
|
109
|
+
test_content = TEMPLATE_TEST.format(name=name, filename=statute_file.name)
|
|
110
|
+
test_file.write_text(test_content, encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
click.echo(colorize(f"Created Yuho project: {project_dir}", Colors.CYAN + Colors.BOLD))
|
|
113
|
+
click.echo(f" {statute_file.name}")
|
|
114
|
+
click.echo(f" metadata.toml")
|
|
115
|
+
click.echo(f" tests/test_{safe_name.lower()}.yh")
|
|
116
|
+
click.echo()
|
|
117
|
+
click.echo("Next steps:")
|
|
118
|
+
click.echo(f" 1. Edit {statute_file.name} to define your statute")
|
|
119
|
+
click.echo(f" 2. Run 'yuho check {statute_file}' to validate")
|
|
120
|
+
click.echo(f" 3. Run 'yuho transpile {statute_file} -t english' to see English version")
|