lean-runtime 4.22.0__tar.gz → 4.24.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 (197) hide show
  1. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/CHANGELOG.md +15 -0
  2. {lean_runtime-4.22.0/lean_runtime.egg-info → lean_runtime-4.24.0}/PKG-INFO +1 -1
  3. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/cli.py +108 -15
  4. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/engine.py +109 -2
  5. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/environments.py +17 -4
  6. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/health.py +15 -0
  7. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/lake_cache.py +2 -2
  8. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/oci.py +29 -1
  9. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/project_execution.py +73 -3
  10. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/project_sharing.py +36 -1
  11. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/projects.py +1 -1
  12. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/references.py +1 -1
  13. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/resolver.py +1 -1
  14. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/run_cli.py +12 -6
  15. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/runtime.py +38 -21
  16. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/shared_projects.py +125 -13
  17. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/store.py +58 -0
  18. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/toolchain_oci.py +11 -1
  19. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/toolchains.py +68 -17
  20. {lean_runtime-4.22.0 → lean_runtime-4.24.0/lean_runtime.egg-info}/PKG-INFO +1 -1
  21. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime.egg-info/SOURCES.txt +1 -0
  22. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/pyproject.toml +1 -1
  23. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/cleanup-v1.schema.json +2 -1
  24. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_engine.py +106 -1
  25. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_bundles.py +48 -2
  26. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_cli.py +226 -1
  27. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_events_health.py +1 -0
  28. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_header_cache.py +4 -1
  29. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_interactive.py +3 -0
  30. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_lake_cache.py +9 -0
  31. lean_runtime-4.24.0/tests/test_project_bootstrap.py +106 -0
  32. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_projects.py +124 -14
  33. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_references.py +3 -0
  34. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_run_cli.py +28 -0
  35. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_runtime.py +3 -0
  36. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_store.py +25 -0
  37. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_toolchain_oci.py +3 -0
  38. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_toolchain_slim.py +2 -9
  39. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_toolchains.py +74 -1
  40. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/LICENSE +0 -0
  41. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/MANIFEST.in +0 -0
  42. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/README.md +0 -0
  43. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/environments.toml +0 -0
  44. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  45. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  46. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  47. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  48. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  49. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  50. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  51. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  52. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  53. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/catalog/locks/mathlib-v4.33.1.lock.json +0 -0
  54. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/README.md +0 -0
  55. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  56. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/mathlib-4.32.2.json +0 -0
  57. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/mathlib-4.32.2.toml +0 -0
  58. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/mathlib-4.33.0.json +0 -0
  59. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/compatibility/mathlib-4.33.0.toml +0 -0
  60. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/concepts/context-selection.md +0 -0
  61. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/index.md +0 -0
  62. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/reference/commands.md +0 -0
  63. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/reference/python-advanced.md +0 -0
  64. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/reference/python-api.md +0 -0
  65. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/reference/storage.md +0 -0
  66. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/trust.md +0 -0
  67. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/tutorial.md +0 -0
  68. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/workflows/check-files.md +0 -0
  69. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/docs/workflows/lake-projects.md +0 -0
  70. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/examples/mathlib.toml +0 -0
  71. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/__init__.py +0 -0
  72. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/__main__.py +0 -0
  73. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/_git.py +0 -0
  74. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/_paths.py +0 -0
  75. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/backends.py +0 -0
  76. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/bundles.py +0 -0
  77. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/capsules.py +0 -0
  78. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/comparison.py +0 -0
  79. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/console.py +0 -0
  80. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/context_resolution.py +0 -0
  81. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/decisions.py +0 -0
  82. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/declaration_index.py +0 -0
  83. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/declaration_index_build.py +0 -0
  84. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/declaration_index_oci.py +0 -0
  85. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/diagnostics.py +0 -0
  86. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/__init__.py +0 -0
  87. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/analyzer.py +0 -0
  88. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/api.py +0 -0
  89. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/candidate.py +0 -0
  90. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/catalog.py +0 -0
  91. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/catalog_build.py +0 -0
  92. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  93. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  94. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/data/catalog.json +0 -0
  95. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/defaults.py +0 -0
  96. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/errors.py +0 -0
  97. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/history.py +0 -0
  98. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/memo.py +0 -0
  99. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/module_inventory.py +0 -0
  100. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/planner.py +0 -0
  101. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/policy.py +0 -0
  102. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/probe.py +0 -0
  103. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/py.typed +0 -0
  104. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/result.py +0 -0
  105. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/schema_resources.py +0 -0
  106. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  107. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  108. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  109. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/errors.py +0 -0
  110. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/events.py +0 -0
  111. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/facade.py +0 -0
  112. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/frontmatter.py +0 -0
  113. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/header_cache.py +0 -0
  114. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/identifier_resolver.py +0 -0
  115. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/import_syntax.py +0 -0
  116. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/lake.py +0 -0
  117. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/lockfiles.py +0 -0
  118. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/locking.py +0 -0
  119. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/matrix.py +0 -0
  120. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/models.py +0 -0
  121. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/oci_protocol.py +0 -0
  122. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/packs.py +0 -0
  123. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/policies.py +0 -0
  124. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/profiling.py +0 -0
  125. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/programs.py +0 -0
  126. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/publisher_verification.py +0 -0
  127. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/py.typed +0 -0
  128. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/schema_resources.py +0 -0
  129. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/serialization.py +0 -0
  130. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/specs.py +0 -0
  131. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/timings.py +0 -0
  132. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/toolchain_slim.py +0 -0
  133. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/verification.py +0 -0
  134. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime/wire.py +0 -0
  135. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  136. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  137. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime.egg-info/requires.txt +0 -0
  138. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/lean_runtime.egg-info/top_level.txt +0 -0
  139. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/mkdocs.yml +0 -0
  140. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/attestation-v1.schema.json +0 -0
  141. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/check-batch-v1.schema.json +0 -0
  142. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/comparison-v1.schema.json +0 -0
  143. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/execution-v1.schema.json +0 -0
  144. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/inspect-v1.schema.json +0 -0
  145. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/matrix-v1.schema.json +0 -0
  146. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/plan-v1.schema.json +0 -0
  147. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/profile-v1.schema.json +0 -0
  148. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/publication-v1.schema.json +0 -0
  149. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/schemas/verify-v1.schema.json +0 -0
  150. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/announcement_gate.py +0 -0
  151. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/catalog_update.py +0 -0
  152. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/lake_cache_gate.py +0 -0
  153. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/prepare_declaration_index.py +0 -0
  154. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/publish_declaration_index.py +0 -0
  155. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/registry_preflight.py +0 -0
  156. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/run_compatibility.py +0 -0
  157. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/run_v1_case_study.py +0 -0
  158. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/scripts/smoke_wheel.py +0 -0
  159. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/setup.cfg +0 -0
  160. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/conftest.py +0 -0
  161. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  162. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_analyzer.py +0 -0
  163. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_catalog.py +0 -0
  164. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_catalog_build.py +0 -0
  165. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  166. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_memo.py +0 -0
  167. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_planner.py +0 -0
  168. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/discovery/test_policy.py +0 -0
  169. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_announcement_gate.py +0 -0
  170. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_backend.py +0 -0
  171. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_capsules.py +0 -0
  172. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_console.py +0 -0
  173. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_context_resolution.py +0 -0
  174. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_declaration_index.py +0 -0
  175. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_diagnostics.py +0 -0
  176. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_environment_integration.py +0 -0
  177. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_facade.py +0 -0
  178. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_frontmatter.py +0 -0
  179. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_git.py +0 -0
  180. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_identifier_resolver.py +0 -0
  181. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_lockfiles.py +0 -0
  182. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_locking.py +0 -0
  183. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_models.py +0 -0
  184. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_multifile.py +0 -0
  185. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_oci_protocol.py +0 -0
  186. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_packs.py +0 -0
  187. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_paths.py +0 -0
  188. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_prebuilt_policy.py +0 -0
  189. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_programs.py +0 -0
  190. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_project_publish.py +0 -0
  191. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_schema_resources.py +0 -0
  192. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_schemas.py +0 -0
  193. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_signatures.py +0 -0
  194. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_specs.py +0 -0
  195. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_v1_precision.py +0 -0
  196. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_verification_inventory.py +0 -0
  197. {lean_runtime-4.22.0 → lean_runtime-4.24.0}/tests/test_verification_probe.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.24.0 - 2026-08-26
6
+
7
+ - Key shared Lake package artifacts by the exact installed Lean and Lake
8
+ executable digests. Existing `/1` package artifacts are source-only donors;
9
+ the first build of each project re-shares artifacts under the `/2` key.
10
+ - Let an exact registered dependency graph donate source across toolchain
11
+ versions, while stripping its build tree, and refresh existing project
12
+ attachments when their computed workspace identity changes.
13
+ - Report obsolete `/1` project artifacts in `doctor`, reclaim them through
14
+ `clean`, and refuse publishing moving toolchain spellings such as `master`
15
+ or undated `nightly`. Publication also fails early when its input is an
16
+ acquired sparse capsule rather than a source build.
17
+
18
+ ## 4.23.0 - 2026-08-24
19
+
5
20
  ## 4.22.0 - 2026-08-23
6
21
 
7
22
  ## 4.21.0 - 2026-08-23
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 4.22.0
3
+ Version: 4.24.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
@@ -119,18 +119,20 @@ def _render_storage(status: StoreStatus) -> None:
119
119
  ("Executions", status.executions, status.executions_bytes),
120
120
  ("Scratch", status.scratch_workspaces, status.scratch_bytes),
121
121
  )
122
+ label_width = max(len(label) for label, _count, _bytes in rows)
123
+ count_width = max(8, *(len(str(count)) for _label, count, _bytes in rows if count is not None))
122
124
  for label, count, bytes_used in rows:
123
- counted = f"{count:>5}" if count is not None else " "
125
+ counted = f"{count:>{count_width}}" if count is not None else " " * count_width
124
126
  size_column = style.cyan(f"{format_byte_size(bytes_used):>10}")
125
- print(f" {label:<15}{counted} {size_column}")
126
- total_label = style.bold(f"{'Materialized':<15}")
127
+ print(f" {label:<{label_width}} {counted} {size_column}")
128
+ total_label = style.bold(f"{'Materialized':<{label_width}}")
127
129
  total_size = style.bold(f"{format_byte_size(status.bytes_used):>10}")
128
130
  free_note = style.dim(f"({format_byte_size(status.bytes_free)} free on disk)")
129
- print(f" {total_label} {total_size} {free_note}")
131
+ print(f" {total_label} {' ' * count_width} {total_size} {free_note}")
130
132
  if status.allocated_bytes:
131
- allocated_label = style.bold(f"{'Allocated*':<15}")
133
+ allocated_label = style.bold(f"{'Allocated*':<{label_width}}")
132
134
  allocated_size = style.bold(f"{format_byte_size(status.allocated_bytes):>10}")
133
- print(f" {allocated_label} {allocated_size}")
135
+ print(f" {allocated_label} {' ' * count_width} {allocated_size}")
134
136
  print(style.dim(" * hard links counted once; copy-on-write blocks may still be shared"))
135
137
  if status.environment_usage:
136
138
  print()
@@ -177,6 +179,7 @@ def _render_cleanup(
177
179
  environments: CleanupReport,
178
180
  downloads: DownloadCleanupReport | None,
179
181
  scratch: CleanupReport | None = None,
182
+ project_artifacts: CleanupReport | None = None,
180
183
  ) -> None:
181
184
  style = styler_for(sys.stdout)
182
185
  retained_note = style.dim(
@@ -218,6 +221,17 @@ def _render_cleanup(
218
221
  )
219
222
  else:
220
223
  print("No abandoned workspaces to remove.")
224
+ if project_artifacts is not None:
225
+ if project_artifacts.candidates:
226
+ anything = True
227
+ print(
228
+ style.bold(
229
+ f"Would remove {len(project_artifacts.candidates)} obsolete project "
230
+ f"artifact tree(s) · {format_byte_size(project_artifacts.candidate_bytes)}"
231
+ )
232
+ )
233
+ else:
234
+ print("No obsolete /1 project artifacts to remove.")
221
235
  print(retained_note)
222
236
  if anything:
223
237
  print()
@@ -226,10 +240,13 @@ def _render_cleanup(
226
240
  reclaimed = environments.reclaimed_bytes + (downloads.reclaimed_bytes if downloads else 0)
227
241
  if scratch is not None:
228
242
  reclaimed += scratch.reclaimed_bytes
243
+ if project_artifacts is not None:
244
+ reclaimed += project_artifacts.reclaimed_bytes
229
245
  removed = (
230
246
  len(environments.removed)
231
247
  + (len(downloads.removed) if downloads else 0)
232
248
  + (len(scratch.removed) if scratch else 0)
249
+ + (len(project_artifacts.removed) if project_artifacts else 0)
233
250
  )
234
251
  if removed:
235
252
  parts = [f"{len(environments.removed)} environment(s)"]
@@ -237,6 +254,8 @@ def _render_cleanup(
237
254
  parts.append(f"{len(downloads.removed)} cached download(s)")
238
255
  if scratch is not None:
239
256
  parts.append(f"{len(scratch.removed)} abandoned workspace(s)")
257
+ if project_artifacts is not None:
258
+ parts.append(f"{len(project_artifacts.removed)} obsolete project artifact tree(s)")
240
259
  print(
241
260
  style.green(f"Removed {' and '.join(parts)} · reclaimed {format_byte_size(reclaimed)}")
242
261
  )
@@ -886,6 +905,13 @@ Advanced namespaces:
886
905
  adopt.add_argument(
887
906
  "--dry-run", action="store_true", help="show the adoption plan without changing anything"
888
907
  )
908
+ adopt.add_argument(
909
+ "--no-agents",
910
+ dest="agents",
911
+ action="store_false",
912
+ default=True,
913
+ help="skip generating AGENTS.md",
914
+ )
889
915
  _add_common_output(adopt)
890
916
  adopt.set_defaults(command="attach", recursive=None, execute=False)
891
917
 
@@ -1150,6 +1176,13 @@ Advanced namespaces:
1150
1176
  pshare.add_argument(
1151
1177
  "--dry-run", action="store_true", help="show the sharing plan without changing anything"
1152
1178
  )
1179
+ pshare.add_argument(
1180
+ "--no-agents",
1181
+ dest="agents",
1182
+ action="store_false",
1183
+ default=True,
1184
+ help="skip generating AGENTS.md",
1185
+ )
1153
1186
  _add_common_output(pshare)
1154
1187
  pshare.set_defaults(command="attach", recursive=None, execute=False)
1155
1188
  punshare = pc.add_parser("unshare", help="materialize independent dependency copies")
@@ -1679,9 +1712,19 @@ def main(argv: list[str] | None = None) -> int:
1679
1712
  if args.agents:
1680
1713
  print(f"Agent guide: {init_result.root / 'AGENTS.md'}")
1681
1714
  project_name = init_plan.project_name or init_result.root.name
1682
- print(
1683
- f"Next: cd {init_result.root} && lean-runtime check {project_name}/Basic.lean"
1715
+ # Lake capitalizes the module directory during `lake init`, so derive
1716
+ # the hint from the file it actually created rather than the raw name.
1717
+ created = sorted(
1718
+ path
1719
+ for path in init_result.root.glob("*/Basic.lean")
1720
+ if not path.parent.name.startswith(".")
1721
+ )
1722
+ module_file = (
1723
+ created[0].relative_to(init_result.root)
1724
+ if created
1725
+ else Path(project_name) / "Basic.lean"
1684
1726
  )
1727
+ print(f"Next: cd {init_result.root} && lean-runtime check {module_file}")
1685
1728
  return 0
1686
1729
  if args.command == "scan":
1687
1730
  scan_result = runtime.scan_projects(args.path, recursive=args.recursive)
@@ -1793,8 +1836,24 @@ def main(argv: list[str] | None = None) -> int:
1793
1836
  recursive=args.recursive,
1794
1837
  plan=adoption_plan,
1795
1838
  )
1839
+ # Adoption is the canonical moment a repository becomes agent-ready,
1840
+ # including re-runs against already-attached projects.
1841
+ agent_guides = (
1842
+ [
1843
+ guide
1844
+ for attached in adoption_result.results
1845
+ if (guide := runtime.write_agents_guide(attached.root)) is not None
1846
+ ]
1847
+ if args.agents
1848
+ else []
1849
+ )
1796
1850
  if args.json:
1797
- _json(adoption_result.to_dict())
1851
+ _json(
1852
+ {
1853
+ **adoption_result.to_dict(),
1854
+ "agent_guides": [str(guide) for guide in agent_guides],
1855
+ }
1856
+ )
1798
1857
  else:
1799
1858
  _render_adoption_plan(adoption_result.plan)
1800
1859
  for attached in adoption_result.results:
@@ -1802,6 +1861,8 @@ def main(argv: list[str] | None = None) -> int:
1802
1861
  f"{attached.action}: {attached.root} · "
1803
1862
  f"{format_byte_size(attached.reclaimed_bytes)} replaced"
1804
1863
  )
1864
+ for guide in agent_guides:
1865
+ print(f"Agent guide: {guide}")
1805
1866
  for root, message in adoption_result.failures:
1806
1867
  print(f"failed: {root}: {message}", file=sys.stderr)
1807
1868
  return 0 if adoption_result.ok else 1
@@ -1958,6 +2019,10 @@ def main(argv: list[str] | None = None) -> int:
1958
2019
  name=args.name,
1959
2020
  build_timeout=args.timeout,
1960
2021
  accelerate=args.accelerate,
2022
+ # Acquire means the whole environment: demanding the root
2023
+ # module keeps a sparse capsule from satisfying it with an
2024
+ # empty closure that only looks ready.
2025
+ import_roots=(ROOT_MODULE,),
1961
2026
  )
1962
2027
  _json(environment.inspect().to_dict())
1963
2028
  if args.timings:
@@ -2252,12 +2317,22 @@ def main(argv: list[str] | None = None) -> int:
2252
2317
  "toolchain": project_status.toolchain,
2253
2318
  "manifest": str(project_status.manifest) if project_status.manifest else None,
2254
2319
  "attached": (project_status.root / "lean-runtime.toml").is_file(),
2320
+ "agents_guide": (
2321
+ str(project_status.root / "AGENTS.md")
2322
+ if (project_status.root / "AGENTS.md").is_file()
2323
+ else None
2324
+ ),
2255
2325
  }
2256
2326
  if args.json:
2257
2327
  _json(status_payload)
2258
2328
  else:
2259
- print(f"{status_payload['kind'].title()}")
2260
- for key, value in status_payload.items():
2329
+ display_payload = dict(status_payload)
2330
+ if display_payload.get("kind") == "project" and (
2331
+ display_payload.get("agents_guide") is None
2332
+ ):
2333
+ display_payload["agents_guide"] = "missing (`lean-runtime adopt` writes one)"
2334
+ print(f"{display_payload['kind'].title()}")
2335
+ for key, value in display_payload.items():
2261
2336
  if key == "kind":
2262
2337
  continue
2263
2338
  label = key.replace("_", " ").title()
@@ -2402,12 +2477,19 @@ def main(argv: list[str] | None = None) -> int:
2402
2477
  dry_run=True,
2403
2478
  minimum_age_seconds=min(args.minimum_age_hours * 3600, 3600),
2404
2479
  )
2480
+ preview_project_artifacts = runtime.clean_legacy_project_artifacts(dry_run=True)
2405
2481
  if not args.json:
2406
- _render_cleanup(cleanup_preview, preview_downloads, preview_scratch)
2482
+ _render_cleanup(
2483
+ cleanup_preview,
2484
+ preview_downloads,
2485
+ preview_scratch,
2486
+ preview_project_artifacts,
2487
+ )
2407
2488
  has_candidates = bool(
2408
2489
  cleanup_preview.candidates
2409
2490
  or (preview_downloads and preview_downloads.candidates)
2410
2491
  or preview_scratch.candidates
2492
+ or preview_project_artifacts.candidates
2411
2493
  )
2412
2494
  if has_candidates and not args.dry_run:
2413
2495
  execute_cleanup = _confirm("Remove these files?", yes=args.yes, json_mode=args.json)
@@ -2428,11 +2510,15 @@ def main(argv: list[str] | None = None) -> int:
2428
2510
  dry_run=not execute_cleanup,
2429
2511
  minimum_age_seconds=min(args.minimum_age_hours * 3600, 3600),
2430
2512
  )
2513
+ gc_project_artifacts = runtime.clean_legacy_project_artifacts(
2514
+ dry_run=not execute_cleanup
2515
+ )
2431
2516
  if args.json:
2432
2517
  gc_payload: dict[str, Any] = {
2433
2518
  "environments": gc_report.to_dict(),
2434
2519
  "downloaded_files": gc_downloads.to_dict() if gc_downloads else None,
2435
2520
  "scratch": gc_scratch.to_dict(),
2521
+ "project_artifacts": gc_project_artifacts.to_dict(),
2436
2522
  }
2437
2523
  _json(
2438
2524
  envelope(
@@ -2442,7 +2528,7 @@ def main(argv: list[str] | None = None) -> int:
2442
2528
  )
2443
2529
  )
2444
2530
  elif execute_cleanup:
2445
- _render_cleanup(gc_report, gc_downloads, gc_scratch)
2531
+ _render_cleanup(gc_report, gc_downloads, gc_scratch, gc_project_artifacts)
2446
2532
  return 0
2447
2533
  if args.command == "check":
2448
2534
  if args.across is not None:
@@ -2547,7 +2633,7 @@ def main(argv: list[str] | None = None) -> int:
2547
2633
  watched = Path(args.inputs[0]).expanduser().resolve()
2548
2634
  if not watched.is_file():
2549
2635
  raise ValueError(f"watched Lean file does not exist: {watched}")
2550
- print(f"Watching {watched} · Ctrl-C to stop")
2636
+ print(f"Watching {watched} · Ctrl-C to stop", flush=True)
2551
2637
  if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
2552
2638
  runtime.header_cache.enabled = True
2553
2639
  previous: tuple[int, int] | None = None
@@ -2563,6 +2649,7 @@ def main(argv: list[str] | None = None) -> int:
2563
2649
  policy=_policy(args),
2564
2650
  )
2565
2651
  _emit_result(watched_result, False)
2652
+ sys.stdout.flush()
2566
2653
  time.sleep(args.watch_interval)
2567
2654
  check_subject = (
2568
2655
  Path(args.inputs[-1]).name
@@ -2614,10 +2701,16 @@ def main(argv: list[str] | None = None) -> int:
2614
2701
  if args.include:
2615
2702
  raise ValueError("local project checks do not accept --include")
2616
2703
  display_path = "<stdin>"
2704
+ stdin_project = args.project
2705
+ if stdin_project is None and args.toolchain is None:
2706
+ try:
2707
+ stdin_project = discover_project(Path.cwd()).root
2708
+ except ProjectNotFoundError:
2709
+ stdin_project = None
2617
2710
  result = runtime.check(
2618
2711
  sys.stdin.read(),
2619
2712
  toolchain=args.toolchain,
2620
- project=args.project,
2713
+ project=stdin_project,
2621
2714
  policy=_policy(args),
2622
2715
  )
2623
2716
  source_file = None
@@ -2,11 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import threading
6
7
  import time
7
8
 
8
9
  from ..errors import LeanRuntimeError
9
- from .candidate import DiscoveryPlan
10
+ from ..models import ExecutionResult
11
+ from .candidate import Candidate, DiscoveryPlan
12
+ from .catalog import CatalogEntry
10
13
  from .probe import CandidateProbe, ProbeIntegrityFailure, ProbeUnavailable
11
14
  from .result import (
12
15
  AttemptStatus,
@@ -27,6 +30,8 @@ DISCOVERY_EXHAUSTED = "DISCOVERY_EXHAUSTED"
27
30
  DISCOVERY_EXPLICIT_LOCK = "DISCOVERY_EXPLICIT_LOCK"
28
31
  DISCOVERY_CANCELLED = "DISCOVERY_CANCELLED"
29
32
  DISCOVERY_RUNTIME_ERROR = "DISCOVERY_RUNTIME_ERROR"
33
+ DISCOVERY_MODULE_UNAVAILABLE = "DISCOVERY_MODULE_UNAVAILABLE"
34
+ DISCOVERY_VERDICT_BOUNDED = "DISCOVERY_VERDICT_BOUNDED"
30
35
  CANDIDATE_LEAN_REJECTED = "CANDIDATE_LEAN_REJECTED"
31
36
  CANDIDATE_UNAVAILABLE = "CANDIDATE_UNAVAILABLE"
32
37
  CANDIDATE_INTEGRITY_FAILURE = "CANDIDATE_INTEGRITY_FAILURE"
@@ -145,6 +150,63 @@ def _rejection_detail(stderr: str, stdout: str) -> str:
145
150
  return stderr.strip() or stdout.strip() or "Lean rejected the source"
146
151
 
147
152
 
153
+ # Lean reports a missing import as `unknown module prefix 'X'` (directory-shaped
154
+ # roots) or `unknown module 'X'`; both are decidable against candidate
155
+ # inventories without running another compiler.
156
+ _UNKNOWN_MODULE = re.compile(r"unknown module(?: prefix)? '([^']+)'")
157
+ # Identifier-level failures are the rejection class where an older environment
158
+ # legitimately changes the verdict (renamed or deleted lemmas), so they always
159
+ # justify continuing the candidate march.
160
+ _IDENTIFIER_MARKERS = ("unknown identifier", "unknown constant")
161
+ # Positive evidence that the source itself is the problem. Kept deliberately
162
+ # narrow: anything unrecognized preserves the ordinary march.
163
+ _PROOF_LEVEL_MARKERS = ("type mismatch", "unsolved goals", "unexpected token")
164
+
165
+
166
+ def _provides_module(entry: CatalogEntry, name: str) -> bool:
167
+ return name in entry.modules or any(module.startswith(name + ".") for module in entry.modules)
168
+
169
+
170
+ def _rejection_verdict(
171
+ result: ExecutionResult, candidates: tuple[Candidate, ...]
172
+ ) -> tuple[str, tuple[str, ...]]:
173
+ """Classify whether another candidate environment could change a rejection.
174
+
175
+ Returns one of:
176
+ - ``("module_unavailable", names)``: the source imports module roots that no
177
+ planned candidate provides; every further attempt must fail identically.
178
+ - ``("source_verdict", ())``: a proof-level failure (type mismatch,
179
+ unsolved goals, syntax); a second opinion never justifies a fresh
180
+ download — only local environments or an offline plan's source builds.
181
+ - ``("march", ())``: everything else — identifier-level failures, modules
182
+ another candidate does provide, and unclassifiable output all keep the
183
+ ordinary bounded march.
184
+ """
185
+ text = "\n".join(
186
+ (
187
+ *(item.message for item in result.diagnostics),
188
+ result.stderr,
189
+ result.stdout,
190
+ )
191
+ )
192
+ missing = tuple(dict.fromkeys(_UNKNOWN_MODULE.findall(text)))
193
+ if missing:
194
+ unavailable = tuple(
195
+ name
196
+ for name in missing
197
+ if not any(_provides_module(candidate.entry, name) for candidate in candidates)
198
+ )
199
+ if unavailable:
200
+ return "module_unavailable", unavailable
201
+ return "march", ()
202
+ lowered = text.lower()
203
+ if any(marker in lowered for marker in _IDENTIFIER_MARKERS):
204
+ return "march", ()
205
+ if any(marker in lowered for marker in _PROOF_LEVEL_MARKERS):
206
+ return "source_verdict", ()
207
+ return "march", ()
208
+
209
+
148
210
  def discover(
149
211
  source: str,
150
212
  plan: DiscoveryPlan,
@@ -189,6 +251,11 @@ def discover(
189
251
  raise ValueError("an authoritative candidate probe is required")
190
252
  attempts: list[CandidateAttempt] = []
191
253
  remote_acquisitions = 0
254
+ # Set once a proof-level rejection shows the source itself is the problem:
255
+ # remaining candidates are only worth a look when reaching them costs no
256
+ # download (already local, or an offline plan's deliberate source builds).
257
+ local_candidates_only = False
258
+ verdict_bounded_skips = 0
192
259
  for candidate in plan.planned_candidates:
193
260
  if cancel is not None and cancel.is_set():
194
261
  return DiscoveryResult(
@@ -217,6 +284,12 @@ def discover(
217
284
  candidate_is_local = any(
218
285
  reason.code == "RANK_LOCAL_AVAILABLE" for reason in candidate.reasons
219
286
  )
287
+ # A proof-level rejection never justifies a *download*. Offline plans
288
+ # (allow_download=False) acquire candidates by deliberate local source
289
+ # builds, so their march is preserved in full.
290
+ if local_candidates_only and not candidate_is_local and plan.policy.allow_download:
291
+ verdict_bounded_skips += 1
292
+ continue
220
293
  if not candidate_is_local and remote_acquisitions >= plan.policy.max_remote_acquisitions:
221
294
  return DiscoveryResult(
222
295
  status="not_found",
@@ -427,6 +500,29 @@ def discover(
427
500
  duration_seconds=time.monotonic() - started,
428
501
  diagnostics=attempt.diagnostics,
429
502
  )
503
+ if outcome_status == "lean_rejected":
504
+ verdict, unavailable = _rejection_verdict(result, plan.planned_candidates)
505
+ if verdict == "module_unavailable":
506
+ # Statically decidable: no candidate can change this verdict.
507
+ return DiscoveryResult(
508
+ status="not_found",
509
+ confidence="exhausted",
510
+ outcome="source_rejected",
511
+ completion="complete",
512
+ plan=plan,
513
+ attempts=tuple(attempts),
514
+ duration_seconds=time.monotonic() - started,
515
+ diagnostics=(
516
+ ResultDiagnostic(
517
+ DISCOVERY_MODULE_UNAVAILABLE,
518
+ "no cataloged environment provides module(s): "
519
+ + ", ".join(unavailable),
520
+ ),
521
+ *attempt.diagnostics,
522
+ ),
523
+ )
524
+ if verdict == "source_verdict":
525
+ local_candidates_only = True
430
526
 
431
527
  if time.monotonic() >= wall_deadline or probe_remaining <= 0:
432
528
  reason = ResultDiagnostic(DISCOVERY_TIME_LIMIT, "total wall-clock budget expired")
@@ -465,6 +561,17 @@ def discover(
465
561
  if attempt.status == "timeout"
466
562
  for diagnostic in attempt.diagnostics
467
563
  )
564
+ skip_diagnostics = (
565
+ (
566
+ ResultDiagnostic(
567
+ DISCOVERY_VERDICT_BOUNDED,
568
+ f"skipped {verdict_bounded_skips} non-local candidate(s): a proof-level "
569
+ "rejection does not justify downloading further environments",
570
+ ),
571
+ )
572
+ if verdict_bounded_skips
573
+ else ()
574
+ )
468
575
  return DiscoveryResult(
469
576
  status="not_found",
470
577
  confidence=confidence,
@@ -473,5 +580,5 @@ def discover(
473
580
  plan=plan,
474
581
  attempts=tuple(attempts),
475
582
  duration_seconds=time.monotonic() - started,
476
- diagnostics=(*timeout_diagnostics, reason, *rejection_diagnostics),
583
+ diagnostics=(*timeout_diagnostics, reason, *skip_diagnostics, *rejection_diagnostics),
477
584
  )
@@ -766,7 +766,11 @@ class Environment:
766
766
  spawn(
767
767
  resolved_command,
768
768
  cwd=instance,
769
- environment=self.manager.toolchains.environment,
769
+ environment=(
770
+ self.manager.toolchains.environment_for(self.lock.toolchain)
771
+ if requested_command[0] == "lake"
772
+ else self.manager.toolchains.environment
773
+ ),
770
774
  policy=selected_policy,
771
775
  ),
772
776
  )
@@ -905,7 +909,12 @@ class Environment:
905
909
  preliminary: list[BackendResult] = []
906
910
  path_map = {str(instance / name): name for name in files}
907
911
  staging_started = time.monotonic()
908
- execution_environment = self.manager.toolchains.environment
912
+ execution_environment = (
913
+ self.manager.toolchains.environment_for(self.lock.toolchain)
914
+ if operation == "build"
915
+ or (requested_command and requested_command[0] == "lake")
916
+ else self.manager.toolchains.environment
917
+ )
909
918
  if operation == "check":
910
919
  assert entrypoint is not None
911
920
  for name, source in files.items():
@@ -1301,7 +1310,11 @@ class EnvironmentManager:
1301
1310
  result = self.backend.execute(
1302
1311
  command,
1303
1312
  cwd=workspace,
1304
- environment=self.toolchains.environment,
1313
+ environment=(
1314
+ self.toolchains.environment_for(lock.toolchain)
1315
+ if requested_command[0] == "lake"
1316
+ else self.toolchains.environment
1317
+ ),
1305
1318
  policy=build_policy,
1306
1319
  cancel=cancel,
1307
1320
  )
@@ -1327,7 +1340,7 @@ class EnvironmentManager:
1327
1340
  result = self.backend.execute(
1328
1341
  command,
1329
1342
  cwd=workspace,
1330
- environment=self.toolchains.environment,
1343
+ environment=self.toolchains.environment_for(lock.toolchain),
1331
1344
  policy=build_policy,
1332
1345
  cancel=cancel,
1333
1346
  )
@@ -107,6 +107,20 @@ def diagnose(toolchains: ToolchainManager, store: EnvironmentStore) -> DoctorRep
107
107
  )
108
108
  else:
109
109
  checks.append(DoctorCheck("scratch", "pass", "No abandoned workspaces"))
110
+ legacy_artifacts = store.clean_legacy_project_artifacts(dry_run=True)
111
+ if legacy_artifacts.candidates:
112
+ checks.append(
113
+ DoctorCheck(
114
+ "project-artifacts-v1",
115
+ "warning",
116
+ f"{len(legacy_artifacts.candidates)} package artifact tree(s) use the obsolete "
117
+ "toolchain-insensitive /1 key; reclaim them with `lean-runtime clean --yes`",
118
+ )
119
+ )
120
+ else:
121
+ checks.append(
122
+ DoctorCheck("project-artifacts-v1", "pass", "No obsolete /1 project artifacts")
123
+ )
110
124
  store_status = store.status()
111
125
  cutoff = datetime.now(timezone.utc).timestamp() - STALE_ENVIRONMENT_AGE_HOURS * 3600
112
126
  stale = tuple(
@@ -145,6 +159,7 @@ def repair(toolchains: ToolchainManager, store: EnvironmentStore) -> DoctorRepor
145
159
  include_legacy=True,
146
160
  legacy_minimum_age_seconds=LEGACY_SCRATCH_MINIMUM_AGE_SECONDS,
147
161
  )
162
+ store.clean_legacy_project_artifacts(dry_run=False)
148
163
  with suppress(ToolchainError):
149
164
  toolchains.elan_path(bootstrap=True)
150
165
  return diagnose(toolchains, store)
@@ -160,7 +160,7 @@ class LakeArtifactCache:
160
160
  try:
161
161
  process = subprocess.run(
162
162
  self.toolchains.command(toolchain, "lake", *arguments),
163
- env=self.toolchains.environment,
163
+ env=self.toolchains.environment_for(toolchain),
164
164
  text=True,
165
165
  stdout=subprocess.PIPE,
166
166
  stderr=subprocess.STDOUT,
@@ -183,7 +183,7 @@ class LakeArtifactCache:
183
183
 
184
184
  def environment(self, context: ProjectContext) -> dict[str, str]:
185
185
  """Return root-only cache settings without caching dependency packages."""
186
- environment = self.toolchains.environment
186
+ environment = self.toolchains.environment_for(context.toolchain)
187
187
  if not self.project_opted_in(context):
188
188
  return environment
189
189
  self.capabilities(context.toolchain)
@@ -28,7 +28,7 @@ from .bundles import (
28
28
  EnvironmentBundles,
29
29
  _capsule_config_object,
30
30
  )
31
- from .capsules import CapsuleManifest
31
+ from .capsules import CAPSULE_MANIFEST, CapsuleManifest
32
32
  from .errors import (
33
33
  CredentialAcquisitionError,
34
34
  DownloadLimitExceeded,
@@ -62,6 +62,7 @@ from .store import (
62
62
  platform_compatibility,
63
63
  platform_record,
64
64
  )
65
+ from .toolchains import immutable_toolchain_spelling
65
66
 
66
67
  _REPOSITORY = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)+")
67
68
  _BEARER_PARAMETER = re.compile(r'([a-zA-Z]+)="([^"]*)"')
@@ -887,6 +888,17 @@ class OCIEnvironmentCache:
887
888
  capsule = CapsuleManifest.from_dict(config["capsule"])
888
889
  if capsule.lock_id != lock.lock_id or capsule.toolchain != lock.toolchain:
889
890
  raise EnvironmentError("downloadable capsule does not match its lock")
891
+ # A capsule that lacks a requested root cannot satisfy this acquisition;
892
+ # silently projecting a partial closure would yield an environment that
893
+ # claims readiness while missing the modules the caller asked for.
894
+ available = {module.name for module in capsule.modules}
895
+ unavailable = tuple(root for root in roots if root not in available)
896
+ if unavailable:
897
+ raise DownloadUnavailable(
898
+ "downloadable capsule does not provide module(s): " + ", ".join(unavailable),
899
+ reason_code="capsule_roots_unavailable",
900
+ retryable=False,
901
+ )
890
902
  closure = capsule.closure(roots)
891
903
  module_names = tuple(module.name for module in closure)
892
904
  descriptors = {
@@ -1447,6 +1459,22 @@ class OCIEnvironmentPublisher:
1447
1459
  ) -> PublicationInfo:
1448
1460
  if profile != "check-capsule":
1449
1461
  raise ValueError("publication profile must be 'check-capsule'")
1462
+ root = self.store.environment_path(environment_id)
1463
+ try:
1464
+ metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8"))
1465
+ lock = self.store.load_lock(str(metadata["lock_id"]))
1466
+ except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
1467
+ raise EnvironmentError("publication input has invalid environment metadata") from exc
1468
+ if (root / "workspace" / CAPSULE_MANIFEST).is_file():
1469
+ raise EnvironmentError(
1470
+ "publication input must be source-built; sparse acquired capsules cannot be "
1471
+ "republished"
1472
+ )
1473
+ if not immutable_toolchain_spelling(lock.toolchain):
1474
+ raise EnvironmentError(
1475
+ f"refusing to publish mutable toolchain spelling {lock.toolchain!r}; "
1476
+ "use an immutable release or dated nightly"
1477
+ )
1450
1478
  self.check_access()
1451
1479
  with tempfile.TemporaryDirectory(prefix="lean-runtime-publish-") as temporary:
1452
1480
  temporary_root = Path(temporary)