lean-runtime 2.9.1__tar.gz → 2.10.0__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 (188) hide show
  1. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/CHANGELOG.md +28 -0
  2. {lean_runtime-2.9.1/lean_runtime.egg-info → lean_runtime-2.10.0}/PKG-INFO +1 -1
  3. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/cli.md +3 -0
  4. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/environments.md +6 -0
  5. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/local-projects.md +13 -4
  6. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/portable-copies.md +6 -0
  7. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/cli.py +254 -35
  8. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/console.py +19 -0
  9. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/environments.py +4 -10
  10. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/errors.py +25 -0
  11. lean_runtime-2.10.0/lean_runtime/header_cache.py +214 -0
  12. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/health.py +13 -0
  13. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/locking.py +50 -1
  14. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/models.py +6 -0
  15. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/oci.py +96 -13
  16. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/programs.py +2 -6
  17. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/project_execution.py +99 -30
  18. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/project_sharing.py +3 -1
  19. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/resolver.py +5 -2
  20. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/runtime.py +39 -12
  21. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/shared_projects.py +38 -4
  22. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/store.py +122 -0
  23. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/toolchains.py +30 -1
  24. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/wire.py +18 -0
  25. {lean_runtime-2.9.1 → lean_runtime-2.10.0/lean_runtime.egg-info}/PKG-INFO +1 -1
  26. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime.egg-info/SOURCES.txt +2 -0
  27. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/pyproject.toml +1 -1
  28. lean_runtime-2.10.0/schemas/check-batch-v1.schema.json +36 -0
  29. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/cleanup-v1.schema.json +7 -3
  30. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/execution-v1.schema.json +1 -1
  31. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/publication-v1.schema.json +2 -0
  32. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_bundles.py +81 -5
  33. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_cli.py +196 -6
  34. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_console.py +12 -0
  35. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_events_health.py +1 -0
  36. lean_runtime-2.10.0/tests/test_header_cache.py +329 -0
  37. lean_runtime-2.10.0/tests/test_locking.py +59 -0
  38. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_projects.py +7 -0
  39. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_schemas.py +6 -0
  40. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_store.py +35 -0
  41. lean_runtime-2.9.1/lean_runtime/header_cache.py +0 -126
  42. lean_runtime-2.9.1/tests/test_header_cache.py +0 -44
  43. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/LICENSE +0 -0
  44. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/MANIFEST.in +0 -0
  45. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/README.md +0 -0
  46. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/environments.toml +0 -0
  47. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  48. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  49. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  50. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  51. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  52. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  53. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  54. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  55. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  56. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/README.md +0 -0
  57. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  58. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/mathlib-4.32.2.json +0 -0
  59. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/mathlib-4.32.2.toml +0 -0
  60. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/mathlib-4.33.0.json +0 -0
  61. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/compatibility/mathlib-4.33.0.toml +0 -0
  62. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/architecture.md +0 -0
  63. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/captures.md +0 -0
  64. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/case-study-v1.md +0 -0
  65. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/development.md +0 -0
  66. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/getting-started.md +0 -0
  67. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/index.md +0 -0
  68. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/lake-artifact-cache-design.md +0 -0
  69. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/project-publishing.md +0 -0
  70. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/python-api.md +0 -0
  71. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/ready-programs.md +0 -0
  72. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/standalone-files.md +0 -0
  73. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/trust-and-limitations.md +0 -0
  74. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/docs/v1-precision.md +0 -0
  75. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/examples/mathlib.toml +0 -0
  76. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/__init__.py +0 -0
  77. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/__main__.py +0 -0
  78. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/_git.py +0 -0
  79. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/_paths.py +0 -0
  80. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/backends.py +0 -0
  81. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/bundles.py +0 -0
  82. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/capsules.py +0 -0
  83. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/comparison.py +0 -0
  84. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/decisions.py +0 -0
  85. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/diagnostics.py +0 -0
  86. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/__init__.py +0 -0
  87. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/analyzer.py +0 -0
  88. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/api.py +0 -0
  89. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/candidate.py +0 -0
  90. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog.py +0 -0
  91. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_build.py +0 -0
  92. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  93. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  94. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/data/catalog.json +0 -0
  95. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/defaults.py +0 -0
  96. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/engine.py +0 -0
  97. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/errors.py +0 -0
  98. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/module_inventory.py +0 -0
  99. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/planner.py +0 -0
  100. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/policy.py +0 -0
  101. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/probe.py +0 -0
  102. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/py.typed +0 -0
  103. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/result.py +0 -0
  104. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/schema_resources.py +0 -0
  105. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  106. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  107. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  108. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/events.py +0 -0
  109. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/facade.py +0 -0
  110. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/frontmatter.py +0 -0
  111. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/identifier_resolver.py +0 -0
  112. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/lake.py +0 -0
  113. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/lake_cache.py +0 -0
  114. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/lockfiles.py +0 -0
  115. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/matrix.py +0 -0
  116. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/oci_protocol.py +0 -0
  117. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/packs.py +0 -0
  118. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/policies.py +0 -0
  119. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/profiling.py +0 -0
  120. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/projects.py +0 -0
  121. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/publisher_verification.py +0 -0
  122. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/py.typed +0 -0
  123. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/references.py +0 -0
  124. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/run_cli.py +0 -0
  125. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/schema_resources.py +0 -0
  126. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/serialization.py +0 -0
  127. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/specs.py +0 -0
  128. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/timings.py +0 -0
  129. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/toolchain_oci.py +0 -0
  130. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/toolchain_slim.py +0 -0
  131. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime/verification.py +0 -0
  132. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  133. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  134. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime.egg-info/requires.txt +0 -0
  135. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/lean_runtime.egg-info/top_level.txt +0 -0
  136. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/mkdocs.yml +0 -0
  137. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/comparison-v1.schema.json +0 -0
  138. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/inspect-v1.schema.json +0 -0
  139. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/matrix-v1.schema.json +0 -0
  140. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/profile-v1.schema.json +0 -0
  141. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/schemas/verify-v1.schema.json +0 -0
  142. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/announcement_gate.py +0 -0
  143. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/lake_cache_gate.py +0 -0
  144. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/registry_preflight.py +0 -0
  145. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/run_compatibility.py +0 -0
  146. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/run_v1_case_study.py +0 -0
  147. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/scripts/smoke_wheel.py +0 -0
  148. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/setup.cfg +0 -0
  149. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/conftest.py +0 -0
  150. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  151. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_analyzer.py +0 -0
  152. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_catalog.py +0 -0
  153. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_catalog_build.py +0 -0
  154. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  155. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_engine.py +0 -0
  156. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_planner.py +0 -0
  157. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/discovery/test_policy.py +0 -0
  158. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_backend.py +0 -0
  159. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_capsules.py +0 -0
  160. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_diagnostics.py +0 -0
  161. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_environment_integration.py +0 -0
  162. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_facade.py +0 -0
  163. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_frontmatter.py +0 -0
  164. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_git.py +0 -0
  165. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_identifier_resolver.py +0 -0
  166. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_interactive.py +0 -0
  167. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_lake_cache.py +0 -0
  168. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_lockfiles.py +0 -0
  169. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_models.py +0 -0
  170. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_multifile.py +0 -0
  171. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_oci_protocol.py +0 -0
  172. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_packs.py +0 -0
  173. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_paths.py +0 -0
  174. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_prebuilt_policy.py +0 -0
  175. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_programs.py +0 -0
  176. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_project_publish.py +0 -0
  177. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_references.py +0 -0
  178. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_run_cli.py +0 -0
  179. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_runtime.py +0 -0
  180. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_schema_resources.py +0 -0
  181. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_signatures.py +0 -0
  182. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_specs.py +0 -0
  183. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_toolchain_oci.py +0 -0
  184. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_toolchain_slim.py +0 -0
  185. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_toolchains.py +0 -0
  186. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_v1_precision.py +0 -0
  187. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_verification_inventory.py +0 -0
  188. {lean_runtime-2.9.1 → lean_runtime-2.10.0}/tests/test_verification_probe.py +0 -0
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.10.0 - 2026-08-16
6
+
7
+ - Make header snapshots opt-in (`LEAN_RUNTIME_HEADER_SNAPSHOTS=1`; `--watch` and
8
+ `--repeat` enable them automatically), key them by module identity, load
9
+ existing snapshots without holding the creation lock, cancel lock waits
10
+ promptly, and retry once without a snapshot—quarantining it—when a snapshot
11
+ check times out or reports snapshot errors.
12
+ - Support `check FILE...` and `check DIRECTORY...` with independent per-file
13
+ results, `--concurrency`, a `lean-runtime.check-batch/v1` JSON envelope, and
14
+ an explicit `--environment NAME`; the legacy positional `ENVIRONMENT FILE`
15
+ form now applies only when the first argument is not an existing path.
16
+ - Attribute shared workspace lock waits to their holder (PID and operation),
17
+ announce header snapshot waits, and record `workspace_lock` and
18
+ `header_snapshot` phase timings so coordination cost is visible next to the
19
+ actual Lean check time.
20
+
21
+ ## 2.9.2 - 2026-08-16
22
+
23
+ - Keep one fail-closed publication authentication session from push preflight through
24
+ upload, propagate explicit publication timeouts to credential and registry I/O,
25
+ and report credential-provider failures without labeling them anonymous.
26
+ - Use logical project names during atomic initialization, show compact TTY-only check
27
+ progress, cache Lean executable identities across invocations, and report check
28
+ command-preparation timing.
29
+ - Track disposable execution and resolution workspaces with process-held leases;
30
+ include their footprint in storage reporting and reclaim abandoned workspaces with
31
+ `clean` while protecting active work.
32
+
5
33
  ## 2.9.1 - 2026-08-15
6
34
 
7
35
  - Make GHCR credential discovery a single bounded GitHub CLI status read so a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.9.1
3
+ Version: 2.10.0
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -215,6 +215,9 @@ lean-runtime check research-stack Main.lean --include Support/Defs.lean
215
215
  ```
216
216
 
217
217
  Resolution and materialization print structured lifecycle progress to stderr.
218
+ One-shot checks show a compact in-place status on interactive terminals while Lean
219
+ is working. Redirected output, `--json`, and `--quiet` remain free of human progress;
220
+ `--timings` separates command preparation from Lean execution.
218
221
  Pass global `--quiet` before the subcommand to suppress it.
219
222
 
220
223
  Pass global `--timings` before the subcommand for stable phase timing output. Machine-readable
@@ -93,6 +93,12 @@ Short-lived execution leases prevent deletion during cloning without
93
93
  serializing concurrent checks. Locks and source snapshots remain retained in
94
94
  the current store schema.
95
95
 
96
+ Disposable execution and dependency-resolution workspaces carry their own
97
+ process-held ownership leases. `storage` includes them under `Scratch`, and
98
+ `clean` previews and reclaims abandoned workspaces while retaining every
99
+ workspace with a live owner. Legacy scratch directories created before ownership
100
+ metadata are removed only by an explicit `clean --execute` after the safety grace.
101
+
96
102
  Sparse downloads are stored once by artifact digest under the shared module
97
103
  CAS and hardlinked into environment projections when the filesystem permits.
98
104
  `lean-runtime storage` reports this category separately. Because it is shared,
@@ -68,10 +68,19 @@ stays silent. Only root-project outputs enter this cache: locked dependencies
68
68
  continue to use the verified shared workspace, avoiding a second multi-gigabyte
69
69
  copy. `build` retains Lake's complete target semantics.
70
70
 
71
- Repeated file checks reuse Lean's native header snapshots, keyed by the exact
72
- toolchain, project identity, and import block. Body-only edits therefore avoid
73
- reloading the unchanged import environment. `--watch` is a thin save loop over
74
- the same ordinary check/result contract. Failed checks may add exact-graph
71
+ Repeated checks of one file can reuse Lean's native header snapshots, keyed by
72
+ the exact toolchain, project identity, module, and import block. Body-only
73
+ edits therefore avoid reloading the unchanged import environment. Snapshots are
74
+ used automatically by `--watch` and `--repeat`; set
75
+ `LEAN_RUNTIME_HEADER_SNAPSHOTS=1` to enable them for one-shot checks (or `=0`
76
+ to disable them everywhere). A check that fails or times out under a snapshot
77
+ is retried once without it, and the snapshot is discarded. `--watch` is a thin
78
+ save loop over the same ordinary check/result contract. `check` also accepts
79
+ several files or directories and checks each independently, in parallel with
80
+ `--concurrency`. When one invocation must wait on another, the console names
81
+ the holder (for example `Waiting for shared workspace lock held by PID 512
82
+ (shared build of mathlib)`), and `--timings` reports `workspace_lock` and
83
+ `header_snapshot` phases alongside the actual Lean execution time. Failed checks may add exact-graph
75
84
  identifier hints derived from pinned `.ilean` indexes; raw Lean streams remain
76
85
  unchanged.
77
86
 
@@ -135,6 +135,12 @@ run:
135
135
  gh auth refresh -s write:packages,read:packages
136
136
  ```
137
137
 
138
+ Credential discovery is fail-closed: a configured provider that times out or
139
+ returns an unusable token stops publication before any registry request. The same
140
+ verified credential is retained from preflight through upload; it is not rediscovered
141
+ after a long environment build. An explicitly supplied publication `--timeout`
142
+ also applies to credential discovery and registry network operations.
143
+
138
144
  Publish the current platform after ensuring the lock:
139
145
 
140
146
  ```bash
@@ -8,12 +8,14 @@ import os
8
8
  import re
9
9
  import sys
10
10
  import time
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+ from dataclasses import replace
11
13
  from datetime import datetime
12
14
  from importlib.metadata import version as distribution_version
13
15
  from pathlib import Path
14
16
  from typing import Any, cast
15
17
 
16
- from .console import styler_for
18
+ from .console import ConsoleRenderer, styler_for
17
19
  from .environments import ExecutionCapture
18
20
  from .errors import (
19
21
  LeanRuntimeError,
@@ -23,6 +25,7 @@ from .errors import (
23
25
  ResolutionError,
24
26
  )
25
27
  from .events import RuntimeEvent
28
+ from .header_cache import ENABLE_VARIABLE as _HEADER_SNAPSHOTS_VARIABLE
26
29
  from .health import DoctorReport
27
30
  from .lake import ROOT_MODULE
28
31
  from .lockfiles import EnvironmentLock
@@ -39,6 +42,7 @@ from .timings import render_timings
39
42
  from .wire import (
40
43
  envelope,
41
44
  error,
45
+ serialize_check_batch_v1,
42
46
  serialize_comparison_v1,
43
47
  serialize_execution_v1,
44
48
  serialize_matrix_v1,
@@ -89,6 +93,7 @@ def _render_storage(status: StoreStatus) -> None:
89
93
  ("Project packages", status.project_packages, status.project_packages_bytes),
90
94
  ("Toolchains", None, status.toolchains_bytes),
91
95
  ("Executions", status.executions, status.executions_bytes),
96
+ ("Scratch", status.scratch_workspaces, status.scratch_bytes),
92
97
  )
93
98
  for label, count, bytes_used in rows:
94
99
  counted = f"{count:>5}" if count is not None else " "
@@ -139,7 +144,11 @@ def _render_doctor(report: DoctorReport) -> None:
139
144
  print(f"{symbols[check.status]} {check.name}: {check.message}")
140
145
 
141
146
 
142
- def _render_cleanup(environments: CleanupReport, downloads: DownloadCleanupReport | None) -> None:
147
+ def _render_cleanup(
148
+ environments: CleanupReport,
149
+ downloads: DownloadCleanupReport | None,
150
+ scratch: CleanupReport | None = None,
151
+ ) -> None:
143
152
  style = styler_for(sys.stdout)
144
153
  retained_note = style.dim(
145
154
  f"Retained {len(environments.retained)} environment(s) (named, recent, or in use)."
@@ -169,17 +178,36 @@ def _render_cleanup(environments: CleanupReport, downloads: DownloadCleanupRepor
169
178
  )
170
179
  else:
171
180
  print("No unreferenced cached downloads to remove.")
181
+ if scratch is not None:
182
+ if scratch.candidates:
183
+ anything = True
184
+ print(
185
+ style.bold(
186
+ f"Would remove {len(scratch.candidates)} abandoned workspace(s) · "
187
+ f"{format_byte_size(scratch.candidate_bytes)}"
188
+ )
189
+ )
190
+ else:
191
+ print("No abandoned workspaces to remove.")
172
192
  print(retained_note)
173
193
  if anything:
174
194
  print()
175
195
  print("This was a preview. Re-run with --execute to delete.")
176
196
  return
177
197
  reclaimed = environments.reclaimed_bytes + (downloads.reclaimed_bytes if downloads else 0)
178
- removed = len(environments.removed) + (len(downloads.removed) if downloads else 0)
198
+ if scratch is not None:
199
+ reclaimed += scratch.reclaimed_bytes
200
+ removed = (
201
+ len(environments.removed)
202
+ + (len(downloads.removed) if downloads else 0)
203
+ + (len(scratch.removed) if scratch else 0)
204
+ )
179
205
  if removed:
180
206
  parts = [f"{len(environments.removed)} environment(s)"]
181
207
  if downloads is not None:
182
208
  parts.append(f"{len(downloads.removed)} cached download(s)")
209
+ if scratch is not None:
210
+ parts.append(f"{len(scratch.removed)} abandoned workspace(s)")
183
211
  print(
184
212
  style.green(f"Removed {' and '.join(parts)} · reclaimed {format_byte_size(reclaimed)}")
185
213
  )
@@ -290,11 +318,15 @@ def _print_publication_failure(exc: PublicationError) -> None:
290
318
  file=sys.stderr,
291
319
  )
292
320
  print(f" {exc}", file=sys.stderr)
293
- identity = exc.username or "anonymous"
294
- print(
295
- f" Authentication: {identity} (source: {exc.credential_source})",
296
- file=sys.stderr,
297
- )
321
+ if exc.attempted_provider is not None:
322
+ print(" Authentication: unavailable (source: none)", file=sys.stderr)
323
+ print(f" Attempted provider: {exc.attempted_provider}", file=sys.stderr)
324
+ else:
325
+ identity = exc.username or "anonymous"
326
+ print(
327
+ f" Authentication: {identity} (source: {exc.credential_source})",
328
+ file=sys.stderr,
329
+ )
298
330
  if exc.partial:
299
331
  print(
300
332
  " No verified final release was produced; immutable platform content may exist.",
@@ -314,6 +346,82 @@ def _cli_source_name(path: Path) -> str:
314
346
  return path.as_posix()
315
347
 
316
348
 
349
+ def _expand_check_inputs(inputs: list[str]) -> list[Path]:
350
+ """Expand FILE and DIRECTORY inputs into an ordered, deduplicated file list."""
351
+ files: list[Path] = []
352
+ for raw in inputs:
353
+ path = Path(raw)
354
+ if path.is_dir():
355
+ found: list[Path] = []
356
+ for current, directories, filenames in os.walk(path):
357
+ directories[:] = sorted(name for name in directories if not name.startswith("."))
358
+ found.extend(
359
+ Path(current) / name for name in sorted(filenames) if name.endswith(".lean")
360
+ )
361
+ if not found:
362
+ raise ValueError(f"no Lean files found under directory: {path}")
363
+ files.extend(found)
364
+ elif path.is_file():
365
+ files.append(path)
366
+ else:
367
+ raise ValueError(f"check input does not exist: {path}")
368
+ return list(dict.fromkeys(files))
369
+
370
+
371
+ def _run_check_batch(runtime: Runtime, files: list[Path], args: argparse.Namespace) -> int:
372
+ """Check each file independently, optionally in parallel, and report per file."""
373
+ if args.concurrency < 1 or args.concurrency > 32:
374
+ raise ValueError("check concurrency must be between 1 and 32")
375
+ environment = runtime.environment(args.environment) if args.environment else None
376
+
377
+ def execute(path: Path) -> ExecutionResult:
378
+ if environment is not None:
379
+ name = _cli_source_name(path)
380
+ return environment.check_files(
381
+ {name: path.read_text(encoding="utf-8")}, entrypoint=name, policy=_policy(args)
382
+ )
383
+ return runtime.check_file(
384
+ path, toolchain=args.toolchain, project=args.project, policy=_policy(args)
385
+ )
386
+
387
+ started = time.monotonic()
388
+ results: dict[Path, ExecutionResult] = {}
389
+ if args.concurrency == 1:
390
+ for path in files:
391
+ results[path] = execute(path)
392
+ else:
393
+ with ThreadPoolExecutor(
394
+ max_workers=args.concurrency, thread_name_prefix="lean-check"
395
+ ) as pool:
396
+ futures = {pool.submit(execute, path): path for path in files}
397
+ try:
398
+ for future in as_completed(futures):
399
+ results[futures[future]] = future.result()
400
+ except BaseException:
401
+ for future in futures:
402
+ future.cancel()
403
+ raise
404
+ entries = [(str(path), results[path]) for path in files]
405
+ runtime.events.emit(
406
+ "check.completed",
407
+ "Lean check completed",
408
+ ok=all(result.ok for _, result in entries),
409
+ )
410
+ if args.json:
411
+ _json(serialize_check_batch_v1(entries, time.monotonic() - started))
412
+ else:
413
+ for display, result in entries:
414
+ status = "accepted" if result.ok else "rejected"
415
+ print(f"{display}\t{status}\t{result.elapsed_seconds:.2f}s")
416
+ if not result.ok:
417
+ output = "\n".join(part for part in (result.stdout, result.stderr) if part).strip()
418
+ if output:
419
+ print(output, file=sys.stderr)
420
+ accepted = sum(1 for _, result in entries if result.ok)
421
+ print(f"{accepted}/{len(entries)} accepted")
422
+ return 0 if all(result.ok for _, result in entries) else 1
423
+
424
+
317
425
  def _display_result_text(text: str, result: ExecutionResult, display_path: str | None) -> str:
318
426
  """Rewrite only the staged entrypoint when the CLI has a logical input name."""
319
427
  if display_path is None or not result.command:
@@ -436,7 +544,12 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
436
544
  publish_environment.add_argument("--publish-to", required=True)
437
545
  publish_environment.add_argument("--tag", action="append", default=[])
438
546
  publish_environment.add_argument("--name")
439
- publish_environment.add_argument("--timeout", type=float, default=1800)
547
+ publish_environment.add_argument(
548
+ "--timeout",
549
+ type=float,
550
+ default=None,
551
+ help="override timeouts for credential, registry, hydration, and build operations",
552
+ )
440
553
  publish_environment.add_argument("--platform-only", action="store_true")
441
554
  publish_environment.add_argument("--accelerate", action="store_true")
442
555
  publish_environment.add_argument("--sign", action="store_true")
@@ -511,8 +624,8 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
511
624
  push.add_argument(
512
625
  "--timeout",
513
626
  type=float,
514
- default=1800,
515
- help="maximum seconds for each artifact hydration and the environment build",
627
+ default=None,
628
+ help="override timeouts for credential, registry, hydration, and build operations",
516
629
  )
517
630
  push.add_argument(
518
631
  "--platform-only",
@@ -623,11 +736,36 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
623
736
  program_index.add_argument("--tag", action="append", default=[])
624
737
  program_index.add_argument("--sign", action="store_true")
625
738
 
626
- check = commands.add_parser("check", help="check one Lean file or all local libraries")
739
+ check = commands.add_parser(
740
+ "check",
741
+ help="check Lean files or all local libraries",
742
+ formatter_class=argparse.RawDescriptionHelpFormatter,
743
+ epilog="""examples:
744
+ lean-runtime check check every local library of this project
745
+ lean-runtime check Foo.lean check one file in its discovered project
746
+ lean-runtime check Foo.lean Bar.lean check several files, one result per file
747
+ lean-runtime check src --concurrency 4 check every *.lean under src in parallel
748
+ lean-runtime check Foo.lean --project ~/p check a file against an explicit Lake project
749
+ lean-runtime check --environment NAME Foo.lean
750
+ check inside a published environment
751
+ lean-runtime check Foo.lean --with github:owner/repo@v1.0
752
+ check against exact package references
753
+ lean-runtime check Foo.lean --timeout 60 bound one Lean invocation to 60 seconds
754
+ lean-runtime --timings check Foo.lean append stable phase timings to the result
755
+
756
+ Multi-file checks exit nonzero if any file is rejected. Header snapshots for
757
+ warm re-checks are used by --watch and --repeat automatically; set
758
+ LEAN_RUNTIME_HEADER_SNAPSHOTS=1 to enable them for one-shot checks.""",
759
+ )
627
760
  check.add_argument(
628
761
  "inputs",
629
762
  nargs="*",
630
- help="FILE, legacy ENVIRONMENT FILE, or omit inside a Lake project; FILE may be -",
763
+ help="FILE..., DIRECTORY..., legacy ENVIRONMENT FILE, or omit inside a Lake "
764
+ "project; FILE may be -",
765
+ )
766
+ check.add_argument(
767
+ "--environment",
768
+ help="check the given FILEs inside this published environment",
631
769
  )
632
770
  check.add_argument(
633
771
  "--with",
@@ -652,7 +790,9 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
652
790
  check.add_argument("--repeat", type=int, help="measure N repeated checks of one project file")
653
791
  check.add_argument("--warmup", type=int, default=1, help="warmups before --repeat samples")
654
792
  check.add_argument("--across", type=Path, help="check FILE across contexts in CONFIGURATION")
655
- check.add_argument("--concurrency", type=int, default=1, help="workers for --across")
793
+ check.add_argument(
794
+ "--concurrency", type=int, default=1, help="workers for --across and multi-file checks"
795
+ )
656
796
  _add_policy(check)
657
797
 
658
798
  inspect = commands.add_parser("inspect", help="inspect a published environment")
@@ -933,6 +1073,10 @@ def main(argv: list[str] | None = None) -> int:
933
1073
  args.mathlib = None if args.core else args.mathlib_version
934
1074
  operation_started = time.monotonic()
935
1075
  display_path: str | None = None
1076
+ renderer = ConsoleRenderer(
1077
+ mode="quiet" if args.quiet or getattr(args, "json", False) else None,
1078
+ verbose=args.verbose,
1079
+ )
936
1080
  try:
937
1081
  if args.command == "completion":
938
1082
  print(_completion_script(args.shell), end="")
@@ -947,7 +1091,7 @@ def main(argv: list[str] | None = None) -> int:
947
1091
  )
948
1092
  runtime = Runtime(
949
1093
  home=args.home,
950
- on_event=None if args.quiet else _progress,
1094
+ on_event=renderer,
951
1095
  availability=selected_availability,
952
1096
  libraries=args.libraries,
953
1097
  max_download_bytes=selected_download_limit,
@@ -1190,7 +1334,13 @@ def main(argv: list[str] | None = None) -> int:
1190
1334
  _json(environment.inspect().to_dict())
1191
1335
  return 0
1192
1336
  if args.command == "build-and-publish":
1193
- access = runtime.check_publication_access(args.publish_to)
1337
+ timeout_override = args.timeout
1338
+ publisher = runtime.begin_publication(
1339
+ args.publish_to,
1340
+ auth_timeout=timeout_override if timeout_override is not None else 10,
1341
+ registry_timeout=timeout_override if timeout_override is not None else 30,
1342
+ )
1343
+ access = publisher.check_access()
1194
1344
  if args.check_access:
1195
1345
  _json(
1196
1346
  envelope(_schema_for(args.command), ok=True, data=access.to_dict())
@@ -1203,7 +1353,7 @@ def main(argv: list[str] | None = None) -> int:
1203
1353
  environment = runtime.open_exact(
1204
1354
  EnvironmentLock.load(args.lock),
1205
1355
  name=args.name,
1206
- build_timeout=args.timeout,
1356
+ build_timeout=timeout_override if timeout_override is not None else 1800,
1207
1357
  accelerate=args.accelerate,
1208
1358
  )
1209
1359
  publication = runtime.publish_environment(
@@ -1213,6 +1363,7 @@ def main(argv: list[str] | None = None) -> int:
1213
1363
  finalize=not args.platform_only,
1214
1364
  sign=args.sign,
1215
1365
  attest=args.attest,
1366
+ publisher=publisher,
1216
1367
  ).to_dict()
1217
1368
  publication["consumer_command"] = (
1218
1369
  f"lean-runtime --library {args.publish_to} download {args.lock}"
@@ -1487,10 +1638,15 @@ def main(argv: list[str] | None = None) -> int:
1487
1638
  if args.include_downloads
1488
1639
  else None
1489
1640
  )
1641
+ gc_scratch = runtime.clean_scratch(
1642
+ dry_run=not args.execute,
1643
+ minimum_age_seconds=min(args.minimum_age_hours * 3600, 3600),
1644
+ )
1490
1645
  if args.json:
1491
1646
  gc_payload: dict[str, Any] = {
1492
1647
  "environments": gc_report.to_dict(),
1493
1648
  "downloaded_files": gc_downloads.to_dict() if gc_downloads else None,
1649
+ "scratch": gc_scratch.to_dict(),
1494
1650
  }
1495
1651
  _json(
1496
1652
  envelope(
@@ -1500,11 +1656,17 @@ def main(argv: list[str] | None = None) -> int:
1500
1656
  )
1501
1657
  )
1502
1658
  else:
1503
- _render_cleanup(gc_report, gc_downloads)
1659
+ _render_cleanup(gc_report, gc_downloads, gc_scratch)
1504
1660
  return 0
1505
1661
  if args.command == "check":
1506
1662
  if args.across is not None:
1507
- if len(args.inputs) != 1 or args.package_refs or args.include or args.project:
1663
+ if (
1664
+ len(args.inputs) != 1
1665
+ or args.package_refs
1666
+ or args.include
1667
+ or args.project
1668
+ or args.environment
1669
+ ):
1508
1670
  raise ValueError("check --across requires exactly one FILE")
1509
1671
  across_file = Path(args.inputs[0])
1510
1672
  contexts = load_matrix(args.across)
@@ -1539,6 +1701,8 @@ def main(argv: list[str] | None = None) -> int:
1539
1701
  "check --repeat requires positive samples and nonnegative warmups"
1540
1702
  )
1541
1703
  repeated_file = Path(args.inputs[0])
1704
+ if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
1705
+ runtime.header_cache.enabled = True
1542
1706
  for _ in range(args.warmup):
1543
1707
  warm = runtime.check_file(
1544
1708
  repeated_file, project=args.project, policy=_policy(args)
@@ -1576,6 +1740,8 @@ def main(argv: list[str] | None = None) -> int:
1576
1740
  if not watched.is_file():
1577
1741
  raise ValueError(f"watched Lean file does not exist: {watched}")
1578
1742
  print(f"Watching {watched} · Ctrl-C to stop")
1743
+ if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
1744
+ runtime.header_cache.enabled = True
1579
1745
  previous: tuple[int, int] | None = None
1580
1746
  while True:
1581
1747
  stat = watched.stat()
@@ -1590,11 +1756,34 @@ def main(argv: list[str] | None = None) -> int:
1590
1756
  )
1591
1757
  _emit_result(watched_result, False)
1592
1758
  time.sleep(args.watch_interval)
1759
+ check_subject = (
1760
+ Path(args.inputs[-1]).name
1761
+ if args.inputs and args.inputs[-1] != "-"
1762
+ else "stdin"
1763
+ if args.inputs
1764
+ else Path(args.project or ".").resolve().name
1765
+ )
1766
+ runtime.events.emit("check.started", "Checking Lean input", subject=check_subject)
1593
1767
  if args.project is not None and args.package_refs:
1594
1768
  raise ValueError("check cannot combine --project with --with")
1769
+ if args.environment is not None and (
1770
+ args.package_refs or args.project or args.toolchain
1771
+ ):
1772
+ raise ValueError(
1773
+ "check --environment cannot be combined with --with, --project, or --toolchain"
1774
+ )
1775
+ legacy_environment = (
1776
+ args.environment is None
1777
+ and not args.package_refs
1778
+ and len(args.inputs) == 2
1779
+ and args.inputs[0] != "-"
1780
+ and not Path(args.inputs[0]).exists()
1781
+ )
1595
1782
  if not args.inputs:
1596
1783
  if args.package_refs or args.include:
1597
1784
  raise ValueError("project-wide check does not accept --with or --include")
1785
+ if args.environment is not None:
1786
+ raise ValueError("check --environment requires at least one FILE")
1598
1787
  result = runtime.check_project(
1599
1788
  args.project or Path("."), toolchain=args.toolchain, policy=_policy(args)
1600
1789
  )
@@ -1604,11 +1793,22 @@ def main(argv: list[str] | None = None) -> int:
1604
1793
  raise ValueError("check with --with expects exactly one FILE")
1605
1794
  environment = runtime.open_references(args.package_refs, toolchain=args.toolchain)
1606
1795
  source_file = Path(args.inputs[0])
1607
- elif len(args.inputs) == 1:
1608
- if args.include:
1609
- raise ValueError("local project checks do not accept --include")
1610
- source_file = Path(args.inputs[0])
1611
- if str(source_file) == "-":
1796
+ elif legacy_environment:
1797
+ if args.toolchain:
1798
+ raise ValueError("check --toolchain is only valid with --with")
1799
+ if args.project:
1800
+ raise ValueError("check --project is only valid with a single FILE")
1801
+ environment = runtime.environment(args.inputs[0])
1802
+ source_file = Path(args.inputs[1])
1803
+ elif "-" in args.inputs:
1804
+ if len(args.inputs) != 1:
1805
+ raise ValueError("stdin (-) must be the only check input")
1806
+ if args.environment is not None:
1807
+ environment = runtime.environment(args.environment)
1808
+ source_file = Path("-")
1809
+ else:
1810
+ if args.include:
1811
+ raise ValueError("local project checks do not accept --include")
1612
1812
  display_path = "<stdin>"
1613
1813
  result = runtime.check(
1614
1814
  sys.stdin.read(),
@@ -1616,23 +1816,26 @@ def main(argv: list[str] | None = None) -> int:
1616
1816
  project=args.project,
1617
1817
  policy=_policy(args),
1618
1818
  )
1819
+ source_file = None
1820
+ else:
1821
+ selected_files = _expand_check_inputs(args.inputs)
1822
+ if len(selected_files) > 1:
1823
+ if args.include:
1824
+ raise ValueError("multi-file checks do not accept --include")
1825
+ return _run_check_batch(runtime, selected_files, args)
1826
+ if args.environment is not None:
1827
+ environment = runtime.environment(args.environment)
1828
+ source_file = selected_files[0]
1619
1829
  else:
1830
+ if args.include:
1831
+ raise ValueError("local project checks do not accept --include")
1620
1832
  result = runtime.check_file(
1621
- source_file,
1833
+ selected_files[0],
1622
1834
  toolchain=args.toolchain,
1623
1835
  project=args.project,
1624
1836
  policy=_policy(args),
1625
1837
  )
1626
- source_file = None
1627
- else:
1628
- if len(args.inputs) != 2:
1629
- raise ValueError("check expects FILE, ENVIRONMENT FILE, or FILE with --with")
1630
- if args.toolchain:
1631
- raise ValueError("check --toolchain is only valid with --with")
1632
- if args.project:
1633
- raise ValueError("check --project is only valid with a single FILE")
1634
- environment = runtime.environment(args.inputs[0])
1635
- source_file = Path(args.inputs[1])
1838
+ source_file = None
1636
1839
  if source_file is None:
1637
1840
  pass
1638
1841
  elif str(source_file) == "-":
@@ -1672,9 +1875,11 @@ def main(argv: list[str] | None = None) -> int:
1672
1875
  shared=args.shared,
1673
1876
  )
1674
1877
  except KeyboardInterrupt:
1878
+ renderer.close()
1675
1879
  print("lean-runtime: interrupted", file=sys.stderr)
1676
1880
  return 130
1677
1881
  except (ResolutionError, MaterializationError) as exc:
1882
+ renderer.close()
1678
1883
  details = {
1679
1884
  "phase": exc.phase,
1680
1885
  "command": list(exc.command),
@@ -1694,6 +1899,7 @@ def main(argv: list[str] | None = None) -> int:
1694
1899
  _print_operation_failure(exc, verbose=args.verbose)
1695
1900
  return 2
1696
1901
  except PublicationError as exc:
1902
+ renderer.close()
1697
1903
  if getattr(args, "json", False):
1698
1904
  _json(
1699
1905
  envelope(
@@ -1707,6 +1913,7 @@ def main(argv: list[str] | None = None) -> int:
1707
1913
  _print_publication_failure(exc)
1708
1914
  return exc.exit_code
1709
1915
  except (LeanRuntimeError, OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
1916
+ renderer.close()
1710
1917
  if getattr(args, "json", False):
1711
1918
  _json(
1712
1919
  envelope(
@@ -1719,6 +1926,18 @@ def main(argv: list[str] | None = None) -> int:
1719
1926
  else:
1720
1927
  print(f"lean-runtime: {exc}", file=sys.stderr)
1721
1928
  return 2
1929
+ if args.command in {"check", "check-file"}:
1930
+ runtime.events.emit("check.completed", "Lean check completed", ok=result.ok)
1931
+ total_ms = round((time.monotonic() - operation_started) * 1000)
1932
+ accounted_ms = sum(timing.duration_ms for timing in result.timings)
1933
+ result = replace(
1934
+ result,
1935
+ timings=(
1936
+ PhaseTiming("command_preparation", max(0, total_ms - accounted_ms)),
1937
+ *result.timings,
1938
+ ),
1939
+ )
1940
+ renderer.close()
1722
1941
  _emit_result(result, args.json, display_path=display_path)
1723
1942
  if args.timings:
1724
1943
  print(render_timings(result.timings), file=sys.stderr)
@@ -215,6 +215,25 @@ class ConsoleRenderer:
215
215
  def _render_capability_required(self, event: RuntimeEvent) -> None:
216
216
  self._print(event.message)
217
217
 
218
+ def _render_check_started(self, event: RuntimeEvent) -> None:
219
+ if self.mode != "tty":
220
+ return
221
+ subject = str(event.data.get("subject") or "Lean input")
222
+ message = f"Checking {subject}…"
223
+ self.stream.write("\r" + self.style.cyan(message))
224
+ self.stream.flush()
225
+ self._line_length = len(message)
226
+
227
+ def _render_check_completed(self, _event: RuntimeEvent) -> None:
228
+ self._end_progress_line()
229
+
230
+ def _render_check_header_wait(self, event: RuntimeEvent) -> None:
231
+ module = str(event.data.get("module") or "Lean input")
232
+ self._print(f"Waiting for header snapshot initialization: {module}")
233
+
234
+ def _render_project_workspace_lock_wait(self, event: RuntimeEvent) -> None:
235
+ self._print(event.message)
236
+
218
237
  # -- low-level output -----------------------------------------------
219
238
 
220
239
  def _verbose_line(self, event: RuntimeEvent) -> None: