leanback 0.1.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.
leanback/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # LeanBack Implementation
2
+
3
+ MCP server exposing Lean 4 development tools via Model Context Protocol.
4
+
5
+ ## Module Structure
6
+
7
+ - `mcp_server.py`: MCP server — defines project_info, check, prove, goals, eval, search tools
8
+ - `check.py`: Check tool implementation
9
+ - `prove.py`: Prove tool implementation
10
+ - `goals.py`: Goals tool implementation (proof state inspection)
11
+ - `mathlib_search.py`: Search tool (Mathlib declaration search)
12
+ - `mathlib_search_simple.py`: Regex-based Mathlib search backend
13
+ - `mcp_test.py`: E2E test client
14
+ - `common/`: Shared utilities
15
+ - `env.py`: Lean environment handling (LeanRunner)
16
+ - `errors.py`: Error parsing and formatting
17
+ - `project_context.py`: Project validation and context
18
+ - `path_manager.py`: Path resolution and management
19
+ - `util.py`: Misc utilities (temp file creation)
leanback/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """LeanBack - Lean 4 development tools for Claude Code."""
2
+
3
+ __version__ = "0.3.0"
leanback/check.py ADDED
@@ -0,0 +1,365 @@
1
+ """
2
+ Implementation of the leanback-check tool.
3
+
4
+ This module provides functionality for checking Lean files and projects.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from rich.console import Console
12
+
13
+ from leanback.common.env import LeanRunner
14
+ from leanback.common.errors import LeanError, parse_lean_output
15
+ from leanback.common.path_manager import PathManager
16
+ from leanback.common.project_context import ProjectContext
17
+ from leanback.common.util import create_temp_file
18
+
19
+
20
+ def check_file(
21
+ file_path: str | Path,
22
+ project_path: str | Path | None = None,
23
+ capture_json: bool = True,
24
+ console: Console | None = None,
25
+ preserve_relative: bool = True,
26
+ ) -> tuple[bool, list[LeanError]]:
27
+ """
28
+ Check a single Lean file for errors.
29
+
30
+ Args:
31
+ file_path: Path to the Lean file to check
32
+ project_path: Path to the Lean project (if None, tries to find it)
33
+ capture_json: Whether to capture output as JSON for better error parsing
34
+ console: Rich console for output (creates a new one if None)
35
+ preserve_relative: Whether to preserve relative paths (True by default)
36
+
37
+ Returns:
38
+ Tuple of (success, errors)
39
+ """
40
+ if console is None:
41
+ console = Console()
42
+
43
+ # Convert to Path
44
+ if isinstance(file_path, str):
45
+ file_path = Path(file_path)
46
+
47
+ # Store original path format for consistent return values
48
+ original_file_path = file_path
49
+
50
+ # For file existence check, we need the absolute path
51
+ abs_file_path = file_path.absolute() if preserve_relative else file_path
52
+
53
+ # Ensure the file exists
54
+ if not abs_file_path.exists():
55
+ return False, [
56
+ LeanError(
57
+ file_name=str(
58
+ original_file_path
59
+ ), # Use original path in error messages
60
+ line=0,
61
+ column=0,
62
+ severity="error",
63
+ message=f"File '{original_file_path}' does not exist",
64
+ )
65
+ ]
66
+
67
+ # Get path manager
68
+ path_manager = PathManager()
69
+
70
+ # Find project root if not provided
71
+ if project_path is None:
72
+ project_path = path_manager.find_project_root(file_path)
73
+ else:
74
+ # Use ProjectContext to resolve project path (handles project names)
75
+ project_context = ProjectContext(project_path, console)
76
+ if project_context.project_path:
77
+ project_path = project_context.project_path
78
+ else:
79
+ # If ProjectContext couldn't resolve it, try as a regular path
80
+ if isinstance(project_path, str):
81
+ project_path = Path(project_path)
82
+ if not project_path.exists():
83
+ return False, [
84
+ LeanError(
85
+ file_name=str(original_file_path),
86
+ line=0,
87
+ column=0,
88
+ severity="error",
89
+ message=f"Project '{project_path}' not found",
90
+ )
91
+ ]
92
+
93
+ # Check if this file imports mathlib
94
+ has_mathlib_import = path_manager.file_has_mathlib_import(file_path)
95
+
96
+ # Always enable mathlib integration with all dependencies for consistent behavior
97
+ # Run Lean on the file
98
+ exit_code, stdout, stderr = LeanRunner.run_lean(
99
+ file_path=file_path,
100
+ project_path=project_path,
101
+ capture_json=capture_json,
102
+ check=False,
103
+ )
104
+
105
+ # Parse the output for errors
106
+ errors = parse_lean_output(
107
+ stdout if capture_json else stderr, json_format=capture_json
108
+ )
109
+
110
+ # If no detailed errors were parsed but the check failed, provide more information
111
+ if exit_code != 0 and not errors:
112
+ # First, check if it might be a mathlib import issue
113
+ if has_mathlib_import:
114
+ errors.append(
115
+ LeanError(
116
+ file_name=str(file_path),
117
+ line=0,
118
+ column=0,
119
+ severity="error",
120
+ message="Mathlib import issue detected.",
121
+ suggestion="\nTo fix mathlib import issues:\n"
122
+ "1. Make sure your file is part of a properly created project\n"
123
+ "2. Create a project with Mathlib: lake +leanprover-community/mathlib4:lean-toolchain new <name> math\n"
124
+ "3. Download cache: cd <name> && lake exe cache get\n\n"
125
+ "Raw output from Lean:\n"
126
+ + (
127
+ f"stdout: {stdout}\n\nstderr: {stderr}"
128
+ if stdout or stderr
129
+ else "No output captured"
130
+ ),
131
+ )
132
+ )
133
+ else:
134
+ # General error with no specific details
135
+ errors.append(
136
+ LeanError(
137
+ file_name=str(file_path),
138
+ line=0,
139
+ column=0,
140
+ severity="error",
141
+ message="Lean check failed without detailed error information",
142
+ suggestion="\nRaw output from Lean:\n"
143
+ + (
144
+ f"stdout: {stdout}\n\nstderr: {stderr}"
145
+ if stdout or stderr
146
+ else "No output captured"
147
+ ),
148
+ )
149
+ )
150
+
151
+ # Return success (no errors) and the list of errors
152
+ return exit_code == 0, errors
153
+
154
+
155
+ def check_project(
156
+ project_path: str | Path,
157
+ check_all_files: bool = False,
158
+ capture_json: bool = True,
159
+ console: Console | None = None,
160
+ ) -> tuple[bool, list[LeanError]]:
161
+ """
162
+ Check an entire Lean project for errors.
163
+
164
+ Args:
165
+ project_path: Path to the Lean project
166
+ check_all_files: Whether to check all .lean files or just run lake build
167
+ capture_json: Whether to capture output as JSON for better error parsing
168
+ console: Rich console for output (creates a new one if None)
169
+
170
+ Returns:
171
+ Tuple of (success, errors)
172
+ """
173
+ if console is None:
174
+ console = Console()
175
+
176
+ # Create project context for validation
177
+ project_context = ProjectContext(project_path, console)
178
+
179
+ # Ensure the project is valid
180
+ if not project_context.project_path:
181
+ return False, [
182
+ LeanError(
183
+ file_name=str(project_path),
184
+ line=0,
185
+ column=0,
186
+ severity="error",
187
+ message=f"Project directory '{project_path}' does not exist",
188
+ )
189
+ ]
190
+
191
+ # Use the validated project path
192
+ project_path = project_context.project_path
193
+
194
+ # If we're not checking individual files, just run lake build
195
+ if not check_all_files:
196
+ console.print(f"Building project at {project_path}...")
197
+ try:
198
+ process = subprocess.run(
199
+ ["lake", "build"],
200
+ cwd=project_path,
201
+ capture_output=True,
202
+ text=True,
203
+ check=False,
204
+ timeout=300, # 5 minutes for mathlib projects
205
+ )
206
+
207
+ # Parse output for actual compilation errors
208
+ errors = []
209
+ has_compilation_errors = False
210
+
211
+ # Combine stdout and stderr for comprehensive parsing
212
+ all_output = (process.stdout or "") + "\n" + (process.stderr or "")
213
+
214
+ for line in all_output.splitlines():
215
+ # Look for Lean compilation errors (more specific patterns)
216
+ if "error:" in line and any(
217
+ indicator in line
218
+ for indicator in [
219
+ ".lean:", # Lean file error
220
+ "failed to", # Compilation failure
221
+ "unknown identifier", # Common Lean error
222
+ "type mismatch", # Type error
223
+ "tactic failed", # Proof error
224
+ "declaration", # Declaration errors
225
+ ]
226
+ ):
227
+ has_compilation_errors = True
228
+ errors.append(
229
+ LeanError(
230
+ file_name="unknown",
231
+ line=0,
232
+ column=0,
233
+ severity="error",
234
+ message=line.strip(),
235
+ )
236
+ )
237
+
238
+ if process.returncode == 0:
239
+ return True, []
240
+
241
+ # Non-zero exit code — always a failure
242
+ if has_compilation_errors:
243
+ return False, errors
244
+
245
+ # No specific compilation errors parsed — provide context
246
+ raw_output = all_output[:1000]
247
+ return False, [
248
+ LeanError(
249
+ file_name="lake",
250
+ line=0,
251
+ column=0,
252
+ severity="error",
253
+ message="Lake build failed.",
254
+ suggestion=(
255
+ "\nThis might be due to:\n"
256
+ "- Dependency or network issues\n"
257
+ "- Corrupted build cache\n\n"
258
+ "Try running 'lake clean' and rebuilding.\n\n"
259
+ f"Raw output:\n{raw_output}"
260
+ ),
261
+ )
262
+ ]
263
+
264
+ except subprocess.TimeoutExpired:
265
+ return False, [
266
+ LeanError(
267
+ file_name="lake",
268
+ line=0,
269
+ column=0,
270
+ severity="error",
271
+ message="Lake build timed out after 5 minutes",
272
+ suggestion="This might be due to:\n"
273
+ "- First-time mathlib download (can take 10-30 minutes)\n"
274
+ "- Large project compilation\n"
275
+ "- System resource constraints\n\n"
276
+ "Try running 'lake build' manually in the project directory.",
277
+ )
278
+ ]
279
+ except Exception as e:
280
+ return False, [
281
+ LeanError(
282
+ file_name="lake",
283
+ line=0,
284
+ column=0,
285
+ severity="error",
286
+ message=f"Failed to run 'lake build': {e}",
287
+ )
288
+ ]
289
+
290
+ # If we're checking individual files, find all .lean files and check them
291
+ all_errors = []
292
+ success = True
293
+
294
+ # Find all .lean files in the project
295
+ lean_files = list(project_path.glob("**/*.lean"))
296
+
297
+ for file_path in lean_files:
298
+ # Skip files in .lake directory
299
+ if ".lake" in file_path.parts:
300
+ continue
301
+
302
+ console.print(f"Checking {file_path.relative_to(project_path)}...")
303
+ file_success, file_errors = check_file(
304
+ file_path=file_path,
305
+ project_path=project_path,
306
+ capture_json=capture_json,
307
+ console=console,
308
+ )
309
+
310
+ if not file_success:
311
+ success = False
312
+ all_errors.extend(file_errors)
313
+
314
+ return success, all_errors
315
+
316
+
317
+ def check_lean_expr(
318
+ expr: str,
319
+ imports: list[str] | None = None,
320
+ project_path: str | Path | None = None,
321
+ capture_json: bool = True,
322
+ console: Console | None = None,
323
+ ) -> tuple[bool, list[LeanError]]:
324
+ """
325
+ Check a Lean expression for errors.
326
+
327
+ Args:
328
+ expr: Lean expression or code to check
329
+ imports: List of imports to include
330
+ project_path: Path to the Lean project (if None, tries to find it)
331
+ capture_json: Whether to capture output as JSON for better error parsing
332
+ console: Rich console for output (creates a new one if None)
333
+
334
+ Returns:
335
+ Tuple of (success, errors)
336
+ """
337
+ if console is None:
338
+ console = Console()
339
+
340
+ # Set default imports if none provided
341
+ if imports is None:
342
+ imports = []
343
+
344
+ # Create a temporary file with the expression
345
+ content = "\n".join(f"import {imp}" for imp in imports)
346
+ if content:
347
+ content += "\n\n"
348
+ content += expr
349
+
350
+ temp_file = create_temp_file(content)
351
+
352
+ try:
353
+ # Check the temporary file
354
+ return check_file(
355
+ file_path=temp_file,
356
+ project_path=project_path,
357
+ capture_json=capture_json,
358
+ console=console,
359
+ )
360
+ finally:
361
+ # Clean up the temporary file
362
+ try:
363
+ os.unlink(temp_file)
364
+ except OSError:
365
+ pass
File without changes
leanback/common/env.py ADDED
@@ -0,0 +1,91 @@
1
+ """
2
+ Lean environment handling utilities.
3
+
4
+ This module provides functionality for executing Lean commands in the correct environment.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+
12
+ class LeanRunner:
13
+ """
14
+ Utility for running Lean commands in the appropriate environment.
15
+ """
16
+
17
+ @staticmethod
18
+ def run_lean(
19
+ file_path: str | Path,
20
+ project_path: str | Path | None = None,
21
+ capture_json: bool = False,
22
+ check: bool = True,
23
+ env_vars: dict[str, str] | None = None,
24
+ timeout: float | None = None,
25
+ ) -> tuple[int, str, str]:
26
+ """
27
+ Run Lean on a file in the appropriate environment.
28
+
29
+ Args:
30
+ file_path: Path to the Lean file to run
31
+ project_path: Path to a Lean project (for lake env)
32
+ capture_json: Whether to request JSON output
33
+ check: Whether to raise an exception on non-zero exit codes
34
+ env_vars: Additional environment variables
35
+ timeout: Timeout in seconds
36
+
37
+ Returns:
38
+ Tuple of (exit_code, stdout, stderr)
39
+ """
40
+ # Convert to Path
41
+ if isinstance(file_path, str):
42
+ file_path = Path(file_path)
43
+
44
+ # We need absolute paths for checking existence internally
45
+ abs_file_path = file_path.absolute()
46
+
47
+ # Determine which path and command to use
48
+ if project_path:
49
+ # If a project is specified, use that project's environment
50
+ project_path = (
51
+ Path(project_path) if isinstance(project_path, str) else project_path
52
+ )
53
+
54
+ # Always use lake env lean for projects - this ensures proper dependency management
55
+ command = ["lake", "env", "lean"]
56
+ if capture_json:
57
+ command.append("--json")
58
+ command.append(str(abs_file_path))
59
+ cwd = project_path
60
+
61
+ # Lake automatically handles all dependency paths - no manual LEAN_PATH setup needed
62
+
63
+ else:
64
+ # Default: run directly with lean for files without project context
65
+ command = ["lean"]
66
+ if capture_json:
67
+ command.append("--json")
68
+ command.append(str(abs_file_path))
69
+
70
+ cwd = file_path.parent
71
+
72
+ # Set up environment
73
+ proc_env = os.environ.copy()
74
+ if env_vars:
75
+ proc_env.update(env_vars)
76
+
77
+ try:
78
+ result = subprocess.run(
79
+ command,
80
+ cwd=cwd,
81
+ check=check,
82
+ capture_output=True,
83
+ text=True,
84
+ env=proc_env,
85
+ timeout=timeout,
86
+ )
87
+ return result.returncode, result.stdout, result.stderr
88
+ except subprocess.CalledProcessError as e:
89
+ return e.returncode, e.stdout, e.stderr
90
+ except subprocess.TimeoutExpired:
91
+ return 124, "", "Lean execution timed out"