lean-runtime 2.0.5__tar.gz → 2.0.6__tar.gz

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 (146) hide show
  1. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/CHANGELOG.md +8 -0
  2. {lean_runtime-2.0.5/lean_runtime.egg-info → lean_runtime-2.0.6}/PKG-INFO +1 -1
  3. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/python-api.md +7 -3
  4. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/ready-programs.md +6 -3
  5. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/engine.py +7 -1
  6. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/result.py +14 -1
  7. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/environments.py +59 -1
  8. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/run_cli.py +9 -5
  9. {lean_runtime-2.0.5 → lean_runtime-2.0.6/lean_runtime.egg-info}/PKG-INFO +1 -1
  10. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/pyproject.toml +1 -1
  11. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/execution-v1.schema.json +1 -1
  12. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_engine.py +26 -1
  13. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_interactive.py +19 -4
  14. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_programs.py +1 -3
  15. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_run_cli.py +71 -5
  16. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_schemas.py +1 -1
  17. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/LICENSE +0 -0
  18. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/MANIFEST.in +0 -0
  19. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/README.md +0 -0
  20. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/catalog/environments.toml +0 -0
  21. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/catalog/locks/core-v4.32.2.lock.json +0 -0
  22. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  23. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  24. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  25. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/compatibility/README.md +0 -0
  26. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/compatibility/mathlib-4.32.2.json +0 -0
  27. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/compatibility/mathlib-4.32.2.toml +0 -0
  28. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/architecture.md +0 -0
  29. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/captures.md +0 -0
  30. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/case-study-v1.md +0 -0
  31. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/cli.md +0 -0
  32. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/development.md +0 -0
  33. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/environments.md +0 -0
  34. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/getting-started.md +0 -0
  35. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/index.md +0 -0
  36. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/local-projects.md +0 -0
  37. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/portable-copies.md +0 -0
  38. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/standalone-files.md +0 -0
  39. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/trust-and-limitations.md +0 -0
  40. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/docs/v1-precision.md +0 -0
  41. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/examples/mathlib.toml +0 -0
  42. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/__init__.py +0 -0
  43. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/__main__.py +0 -0
  44. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/_git.py +0 -0
  45. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/_paths.py +0 -0
  46. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/backends.py +0 -0
  47. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/bundles.py +0 -0
  48. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/cli.py +0 -0
  49. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/comparison.py +0 -0
  50. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/decisions.py +0 -0
  51. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/diagnostics.py +0 -0
  52. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/__init__.py +0 -0
  53. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/analyzer.py +0 -0
  54. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/api.py +0 -0
  55. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/candidate.py +0 -0
  56. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/catalog.py +0 -0
  57. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/catalog_build.py +0 -0
  58. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/catalog_cli.py +0 -0
  59. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/catalog_manifest.py +0 -0
  60. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/data/catalog.json +0 -0
  61. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/defaults.py +0 -0
  62. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/errors.py +0 -0
  63. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/module_inventory.py +0 -0
  64. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/planner.py +0 -0
  65. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/policy.py +0 -0
  66. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/probe.py +0 -0
  67. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/py.typed +0 -0
  68. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/schema_resources.py +0 -0
  69. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  70. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  71. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  72. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/errors.py +0 -0
  73. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/events.py +0 -0
  74. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/facade.py +0 -0
  75. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/frontmatter.py +0 -0
  76. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/health.py +0 -0
  77. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/lake.py +0 -0
  78. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/lockfiles.py +0 -0
  79. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/locking.py +0 -0
  80. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/matrix.py +0 -0
  81. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/models.py +0 -0
  82. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/oci.py +0 -0
  83. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/policies.py +0 -0
  84. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/profiling.py +0 -0
  85. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/programs.py +0 -0
  86. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/projects.py +0 -0
  87. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/publisher_verification.py +0 -0
  88. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/py.typed +0 -0
  89. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/references.py +0 -0
  90. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/resolver.py +0 -0
  91. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/runtime.py +0 -0
  92. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/schema_resources.py +0 -0
  93. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/serialization.py +0 -0
  94. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/specs.py +0 -0
  95. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/store.py +0 -0
  96. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/timings.py +0 -0
  97. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/toolchains.py +0 -0
  98. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/verification.py +0 -0
  99. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime/wire.py +0 -0
  100. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime.egg-info/SOURCES.txt +0 -0
  101. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime.egg-info/dependency_links.txt +0 -0
  102. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime.egg-info/entry_points.txt +0 -0
  103. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime.egg-info/requires.txt +0 -0
  104. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/lean_runtime.egg-info/top_level.txt +0 -0
  105. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/mkdocs.yml +0 -0
  106. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/cleanup-v1.schema.json +0 -0
  107. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/comparison-v1.schema.json +0 -0
  108. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/inspect-v1.schema.json +0 -0
  109. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/matrix-v1.schema.json +0 -0
  110. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/profile-v1.schema.json +0 -0
  111. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/schemas/verify-v1.schema.json +0 -0
  112. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/scripts/run_compatibility.py +0 -0
  113. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/scripts/run_v1_case_study.py +0 -0
  114. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/scripts/smoke_wheel.py +0 -0
  115. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/setup.cfg +0 -0
  116. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/conftest.py +0 -0
  117. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/e2e/test_real_discovery.py +0 -0
  118. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_analyzer.py +0 -0
  119. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_catalog.py +0 -0
  120. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_catalog_build.py +0 -0
  121. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_discovery_schema_resources.py +0 -0
  122. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_planner.py +0 -0
  123. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/discovery/test_policy.py +0 -0
  124. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_backend.py +0 -0
  125. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_bundles.py +0 -0
  126. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_cli.py +0 -0
  127. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_diagnostics.py +0 -0
  128. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_environment_integration.py +0 -0
  129. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_events_health.py +0 -0
  130. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_facade.py +0 -0
  131. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_frontmatter.py +0 -0
  132. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_git.py +0 -0
  133. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_lockfiles.py +0 -0
  134. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_multifile.py +0 -0
  135. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_paths.py +0 -0
  136. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_prebuilt_policy.py +0 -0
  137. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_projects.py +0 -0
  138. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_references.py +0 -0
  139. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_runtime.py +0 -0
  140. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_schema_resources.py +0 -0
  141. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_signatures.py +0 -0
  142. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_specs.py +0 -0
  143. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_store.py +0 -0
  144. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_toolchains.py +0 -0
  145. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_v1_precision.py +0 -0
  146. {lean_runtime-2.0.5 → lean_runtime-2.0.6}/tests/test_verification_inventory.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.0.6
6
+
7
+ - Preserve the highest-ranked Lean compiler rejection when automatic discovery
8
+ exhausts its candidates, including raw output and parsed diagnostics in both
9
+ human and JSON `lean-run` output.
10
+ - Add line-oriented and NDJSON request/response helpers to `InteractiveSession`
11
+ for incremental REPLs and ready-to-run checker programs.
12
+
5
13
  ## 2.0.5
6
14
 
7
15
  - Integrate bounded exact-environment discovery into the `lean-runtime` package.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -179,9 +179,7 @@ with environment.spawn_interactive(
179
179
  policy=ExecutionPolicy(timeout_seconds=3600, memory_mb=4096),
180
180
  ) as session:
181
181
  request = {"id": 1, "method": "get_info", "params": {}}
182
- session.stdin.write(json.dumps(request) + "\n")
183
- session.stdin.flush()
184
- response = json.loads(session.stdout.readline())
182
+ response = session.request_json(request)
185
183
  assert session.running
186
184
 
187
185
  result = session.close()
@@ -199,6 +197,12 @@ the original text is still returned to the caller. Callers of tools that emit
199
197
  substantial data on both streams should drain both streams to avoid ordinary
200
198
  subprocess pipe backpressure.
201
199
 
200
+ For line-oriented tools, `send_line()` and `read_line()` provide incremental
201
+ REPL-style access. `request_line()` serializes one request/response exchange,
202
+ and `request_json()` does the same for NDJSON protocols. The request helpers
203
+ are locked so concurrent callers cannot interleave protocol frames. The raw
204
+ `stdin`, `stdout`, and `stderr` pipes remain available for other protocols.
205
+
202
206
  ## Progress events
203
207
 
204
208
  ```python
@@ -20,11 +20,14 @@ program = runtime.create_program(
20
20
  )
21
21
 
22
22
  with program.spawn_interactive() as session:
23
- session.stdin.write("hello\n")
24
- session.stdin.flush()
25
- print(session.stdout.readline())
23
+ print(session.request_line("hello"))
26
24
  ```
27
25
 
26
+ For an NDJSON checker, `session.request_json({...})` keeps the compiled process
27
+ alive and returns one decoded response per request. This is the fast path for
28
+ large batches: prepare and verify the checker once, then stream compact inputs
29
+ instead of elaborating a new Lean source file for every item.
30
+
28
31
  `create_program` records the program's files and computer compatibility. Opening
29
32
  it later with `runtime.program(program.id)` verifies that neither has changed.
30
33
 
@@ -336,11 +336,17 @@ def discover(
336
336
  DISCOVERY_EXHAUSTED,
337
337
  f"Lean did not accept the source in any of {len(attempts)} tested candidates",
338
338
  )
339
+ rejection_diagnostics = tuple(
340
+ diagnostic
341
+ for attempt in attempts
342
+ if attempt.status == "lean_rejected"
343
+ for diagnostic in attempt.diagnostics
344
+ )
339
345
  return DiscoveryResult(
340
346
  status="not_found",
341
347
  confidence="exhausted",
342
348
  plan=plan,
343
349
  attempts=tuple(attempts),
344
350
  duration_seconds=time.monotonic() - started,
345
- diagnostics=(reason,),
351
+ diagnostics=(reason, *rejection_diagnostics),
346
352
  )
@@ -124,9 +124,22 @@ class DiscoveryResult:
124
124
  compiled = next((item for item in self.attempts if item.status == "compiled"), None)
125
125
  return compiled.environment_id if compiled is not None else None
126
126
 
127
+ @property
128
+ def rejection_attempt(self) -> CandidateAttempt | None:
129
+ """Return the highest-ranked attempt for which Lean rejected the source."""
130
+ return next(
131
+ (
132
+ attempt
133
+ for attempt in self.attempts
134
+ if attempt.status == "lean_rejected" and attempt.execution_result is not None
135
+ ),
136
+ None,
137
+ )
138
+
127
139
  def raise_for_error(self) -> DiscoveryResult:
128
140
  if self.status != "found":
129
- detail = self.diagnostics[0].detail if self.diagnostics else self.status
141
+ details = [item.detail for item in self.diagnostics]
142
+ detail = ": ".join(dict.fromkeys(details)) if details else self.status
130
143
  raise DiscoveryError(f"Lean environment discovery did not succeed: {detail}")
131
144
  return self
132
145
 
@@ -17,7 +17,7 @@ from contextlib import suppress
17
17
  from dataclasses import asdict, dataclass
18
18
  from datetime import datetime, timezone
19
19
  from pathlib import Path, PurePosixPath
20
- from typing import Any, Generic, TextIO, TypeVar, cast
20
+ from typing import Any, Generic, Literal, TextIO, TypeVar, cast
21
21
 
22
22
  from ._git import git_command
23
23
  from ._paths import remove_tree
@@ -291,6 +291,7 @@ class InteractiveSession:
291
291
  self._cleanup = cleanup
292
292
  self._result: ExecutionResult | None = None
293
293
  self._closing = threading.Lock()
294
+ self._io = threading.RLock()
294
295
 
295
296
  @property
296
297
  def stdin(self) -> TextIO:
@@ -316,6 +317,63 @@ class InteractiveSession:
316
317
  """Return the process exit code, or ``None`` while it is running."""
317
318
  return self._process.poll()
318
319
 
320
+ def _reader(self, stream: Literal["stdout", "stderr"]) -> InteractiveTextReader:
321
+ if stream == "stdout":
322
+ return self.stdout
323
+ if stream == "stderr":
324
+ return self.stderr
325
+ raise ValueError("stream must be 'stdout' or 'stderr'")
326
+
327
+ def send_line(self, line: str) -> None:
328
+ """Send and flush one line-oriented protocol request."""
329
+ if not isinstance(line, str):
330
+ raise TypeError("interactive request must be a string")
331
+ if "\n" in line or "\r" in line:
332
+ raise ValueError("interactive request must contain exactly one line")
333
+ with self._io:
334
+ if not self.running:
335
+ raise EnvironmentError("interactive process is not running")
336
+ try:
337
+ self.stdin.write(line + "\n")
338
+ self.stdin.flush()
339
+ except (BrokenPipeError, OSError, ValueError) as exc:
340
+ raise EnvironmentError("interactive process did not accept the request") from exc
341
+
342
+ def read_line(self, *, stream: Literal["stdout", "stderr"] = "stdout") -> str:
343
+ """Read one response line from stdout or stderr."""
344
+ with self._io:
345
+ response = self._reader(stream).readline()
346
+ if response == "":
347
+ raise EnvironmentError(
348
+ f"interactive process ended before producing a {stream} response"
349
+ )
350
+ return response.removesuffix("\n").removesuffix("\r")
351
+
352
+ def request_line(
353
+ self,
354
+ line: str,
355
+ *,
356
+ response_stream: Literal["stdout", "stderr"] = "stdout",
357
+ ) -> str:
358
+ """Atomically send one line and read its corresponding response."""
359
+ with self._io:
360
+ self.send_line(line)
361
+ return self.read_line(stream=response_stream)
362
+
363
+ def request_json(
364
+ self,
365
+ value: object,
366
+ *,
367
+ response_stream: Literal["stdout", "stderr"] = "stdout",
368
+ ) -> Any:
369
+ """Round-trip one newline-delimited JSON value through the live process."""
370
+ request = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
371
+ response = self.request_line(request, response_stream=response_stream)
372
+ try:
373
+ return json.loads(response)
374
+ except json.JSONDecodeError as exc:
375
+ raise EnvironmentError("interactive process returned invalid JSON") from exc
376
+
319
377
  def close(self) -> ExecutionResult:
320
378
  """Send EOF, stop if necessary, persist provenance, and remove the instance."""
321
379
  with self._closing:
@@ -314,14 +314,18 @@ def main(argv: list[str] | None = None) -> int:
314
314
  runtime_events=runtime_events,
315
315
  ).discover_and_check(source)
316
316
  if discovered.status != "found" or discovered.execution_result is None:
317
- raise SpecificationError(_discovery_failure(discovered)) from None
318
- if arguments.lock_out is not None:
319
- assert discovered.lock is not None
320
- discovered.lock.write(arguments.lock_out)
317
+ rejection = discovered.rejection_attempt
318
+ if rejection is None or rejection.execution_result is None:
319
+ raise SpecificationError(_discovery_failure(discovered)) from None
320
+ result = rejection.execution_result
321
+ else:
322
+ if arguments.lock_out is not None:
323
+ assert discovered.lock is not None
324
+ discovered.lock.write(arguments.lock_out)
325
+ result = discovered.execution_result
321
326
  preparation = PhaseTiming(
322
327
  "discovery", round((time.monotonic() - discovery_started) * 1000)
323
328
  )
324
- result = discovered.execution_result
325
329
  if context.lock is not None or context.toolchain is not None or not context.requires:
326
330
  result = replace(result, timings=(preparation, *result.timings))
327
331
  _emit(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.5"
7
+ version = "2.0.6"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -29,7 +29,7 @@
29
29
  "required": ["phase", "duration_ms", "performed"],
30
30
  "additionalProperties": false,
31
31
  "properties": {
32
- "phase": {"enum": ["toolchain", "resolution", "source_acquisition", "cache_lookup", "cache_download", "artifact_hydration", "build", "publication", "environment_open", "instance_creation", "input_staging", "execution", "result_collection", "result_publication", "cleanup"]},
32
+ "phase": {"enum": ["discovery", "toolchain", "resolution", "source_acquisition", "cache_lookup", "cache_download", "artifact_hydration", "build", "publication", "environment_open", "instance_creation", "input_staging", "execution", "result_collection", "result_publication", "cleanup"]},
33
33
  "duration_ms": {"type": "integer", "minimum": 0},
34
34
  "performed": {"type": "boolean"}
35
35
  }
@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
7
7
  import pytest
8
8
 
9
9
  from lean_runtime import ExecutionResult, Runtime
10
- from lean_runtime.discovery import Discovery, DiscoveryPolicy, PolicyError, ProbeOutcome
10
+ from lean_runtime.discovery import (
11
+ Discovery,
12
+ DiscoveryError,
13
+ DiscoveryPolicy,
14
+ PolicyError,
15
+ ProbeOutcome,
16
+ )
11
17
  from lean_runtime.discovery.candidate import Candidate
12
18
  from lean_runtime.discovery.probe import ProbeIntegrityFailure, ProbeUnavailable
13
19
 
@@ -87,6 +93,25 @@ def test_candidate_budget_never_opens_bounded_candidate(sample_catalog) -> None:
87
93
  assert probe.opened == ["mathlib-new"]
88
94
 
89
95
 
96
+ def test_exhaustion_surfaces_the_highest_ranked_lean_rejection(sample_catalog) -> None: # type: ignore[no-untyped-def]
97
+ probe = FakeProbe({"mathlib-new": execution(False, stderr="type mismatch")})
98
+ result = Discovery(
99
+ catalog=sample_catalog,
100
+ policy=DiscoveryPolicy(max_candidates=1),
101
+ probe=probe,
102
+ ).discover_and_check("import Mathlib\n")
103
+
104
+ assert result.status == "not_found"
105
+ assert result.rejection_attempt is result.attempts[0]
106
+ assert [item.code for item in result.diagnostics] == [
107
+ "DISCOVERY_CANDIDATE_LIMIT",
108
+ "CANDIDATE_LEAN_REJECTED",
109
+ ]
110
+ assert result.diagnostics[1].detail == "type mismatch"
111
+ with pytest.raises(DiscoveryError, match="type mismatch"):
112
+ result.raise_for_error()
113
+
114
+
90
115
  def test_unavailable_candidate_advances(sample_catalog) -> None: # type: ignore[no-untyped-def]
91
116
  probe = FakeProbe(
92
117
  {
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import pytest
10
10
 
11
- from lean_runtime import EnvironmentLock, ExecutionPolicy
11
+ from lean_runtime import EnvironmentError, EnvironmentLock, ExecutionPolicy
12
12
  from lean_runtime.backends import LocalBackend
13
13
  from lean_runtime.environments import Environment, EnvironmentManager
14
14
  from lean_runtime.store import EnvironmentStore
@@ -79,9 +79,7 @@ def test_interactive_session_round_trips_and_records_transcript(tmp_path: Path)
79
79
  execution_id = session.execution_id
80
80
  assert session.running
81
81
  assert session.poll() is None
82
- session.stdin.write("hello lean\n")
83
- session.stdin.flush()
84
- assert session.stdout.readline() == "HELLO LEAN\n"
82
+ assert session.request_line("hello lean") == "HELLO LEAN"
85
83
  assert any(environment.manager.store.jobs.iterdir())
86
84
 
87
85
  result = session.close()
@@ -97,6 +95,23 @@ def test_interactive_session_round_trips_and_records_transcript(tmp_path: Path)
97
95
  assert record["operation"] == "interactive"
98
96
 
99
97
 
98
+ def test_interactive_session_round_trips_ndjson(tmp_path: Path) -> None:
99
+ environment = _environment(tmp_path)
100
+ script = (
101
+ "import json, sys\n"
102
+ "for line in sys.stdin:\n"
103
+ " value = json.loads(line)\n"
104
+ " print(json.dumps({'id': value['id'], 'answer': value['value'] + 1}), flush=True)"
105
+ )
106
+ with environment.spawn_interactive([sys.executable, "-u", "-c", script]) as session:
107
+ assert session.request_json({"id": 7, "value": 41}) == {"id": 7, "answer": 42}
108
+ with pytest.raises(ValueError, match="exactly one line"):
109
+ session.send_line("first\nsecond")
110
+
111
+ with pytest.raises(EnvironmentError, match="not running"):
112
+ session.send_line("late request")
113
+
114
+
100
115
  def test_interactive_timeout_is_enforced_and_instance_is_removed(tmp_path: Path) -> None:
101
116
  environment = _environment(tmp_path)
102
117
  session = environment.spawn_interactive(
@@ -34,9 +34,7 @@ def test_program_identity_payload_validation_and_interactive_execution(tmp_path:
34
34
 
35
35
  assert program.id.startswith("program_")
36
36
  with program.spawn_interactive(policy=ExecutionPolicy(timeout_seconds=10)) as session:
37
- session.stdin.write('{"ping":true}\n')
38
- session.stdin.flush()
39
- assert session.stdout.readline() == '{"ping":true}\n'
37
+ assert session.request_json({"ping": True}) == {"ping": True}
40
38
  assert session.close().provenance is not None
41
39
  assert session.close().provenance.program_id == program.id
42
40
 
@@ -1,20 +1,27 @@
1
+ import json
1
2
  from pathlib import Path
2
3
 
3
4
  from lean_runtime import ProjectError
4
- from lean_runtime.models import ExecutionResult
5
+ from lean_runtime.models import Diagnostic, ExecutionResult
5
6
  from lean_runtime.run_cli import main
6
7
 
7
8
 
8
- def _result() -> ExecutionResult:
9
+ def _result(
10
+ *,
11
+ ok: bool = True,
12
+ stderr: str = "",
13
+ diagnostics: tuple[Diagnostic, ...] = (),
14
+ ) -> ExecutionResult:
9
15
  return ExecutionResult(
10
- ok=True,
11
- exit_code=0,
16
+ ok=ok,
17
+ exit_code=0 if ok else 1,
12
18
  toolchain="leanprover/lean4:v4.32.0",
13
19
  command=("lean", "Main.lean"),
14
20
  cwd="/tmp",
15
21
  stdout="",
16
- stderr="",
22
+ stderr=stderr,
17
23
  elapsed_seconds=0.01,
24
+ diagnostics=diagnostics,
18
25
  )
19
26
 
20
27
 
@@ -158,6 +165,65 @@ def test_lean_run_discovers_standalone_file_and_writes_lock(
158
165
  assert "accepted" in capsys.readouterr().out
159
166
 
160
167
 
168
+ def test_lean_run_preserves_discovered_compiler_rejection(
169
+ monkeypatch, tmp_path: Path, capsys
170
+ ) -> None:
171
+ source = tmp_path / "Main.lean"
172
+ source.write_text("import Mathlib\nexample : False := by trivial\n")
173
+ rejection = _result(
174
+ ok=False,
175
+ stderr="Main.lean:2:22: error: tactic 'trivial' failed",
176
+ diagnostics=(
177
+ Diagnostic(
178
+ "error",
179
+ "tactic 'trivial' failed",
180
+ file="Main.lean",
181
+ line=2,
182
+ column=22,
183
+ ),
184
+ ),
185
+ )
186
+
187
+ class NoProjectRuntime(FakeRuntime):
188
+ def __init__(self, **kwargs) -> None:
189
+ super().__init__(**kwargs)
190
+ self.availability = "auto"
191
+ self.libraries = ()
192
+
193
+ def check_file(self, *_args, **_kwargs) -> ExecutionResult:
194
+ raise ProjectError("no project")
195
+
196
+ class Attempt:
197
+ execution_result = rejection
198
+
199
+ class Rejected:
200
+ status = "not_found"
201
+ execution_result = None
202
+ rejection_attempt = Attempt()
203
+
204
+ class FakeDiscovery:
205
+ def __init__(self, **_kwargs) -> None:
206
+ pass
207
+
208
+ def discover_and_check(self, _source: str) -> Rejected:
209
+ return Rejected()
210
+
211
+ monkeypatch.setattr("lean_runtime.run_cli.Runtime", NoProjectRuntime)
212
+ monkeypatch.setattr("lean_runtime.run_cli.Discovery", FakeDiscovery)
213
+
214
+ assert main([str(source), "--quiet"]) == 1
215
+ captured = capsys.readouterr()
216
+ assert "tactic 'trivial' failed" in captured.err
217
+ assert "rejected" in captured.out
218
+
219
+ assert main([str(source), "--json", "--quiet"]) == 1
220
+ payload = json.loads(capsys.readouterr().out)
221
+ assert payload["ok"] is False
222
+ assert payload["data"]["stderr"].endswith("tactic 'trivial' failed")
223
+ assert payload["data"]["diagnostics"][0]["message"] == "tactic 'trivial' failed"
224
+ assert payload["data"]["timings"][0]["phase"] == "discovery"
225
+
226
+
161
227
  def test_lean_run_can_disable_automatic_discovery(monkeypatch, tmp_path: Path, capsys) -> None:
162
228
  source = tmp_path / "Main.lean"
163
229
  source.write_text("import Mathlib\n")
@@ -58,7 +58,7 @@ def test_execution_success_fixture_matches_v1_schema() -> None:
58
58
  stdout="",
59
59
  stderr="",
60
60
  elapsed_seconds=0.01,
61
- timings=(PhaseTiming("execution", 10),),
61
+ timings=(PhaseTiming("discovery", 5), PhaseTiming("execution", 10)),
62
62
  )
63
63
  _validator("execution-v1.schema.json").validate(serialize_execution_v1(result))
64
64
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes