lean-runtime 2.1.0__tar.gz → 2.1.2__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 (151) hide show
  1. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/CHANGELOG.md +30 -0
  2. {lean_runtime-2.1.0/lean_runtime.egg-info → lean_runtime-2.1.2}/PKG-INFO +1 -1
  3. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/development.md +23 -0
  4. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/cli.py +8 -0
  5. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/engine.py +5 -4
  6. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/environments.py +13 -3
  7. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/oci.py +27 -5
  8. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/references.py +14 -0
  9. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/run_cli.py +11 -2
  10. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/runtime.py +2 -0
  11. {lean_runtime-2.1.0 → lean_runtime-2.1.2/lean_runtime.egg-info}/PKG-INFO +1 -1
  12. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/SOURCES.txt +2 -0
  13. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/pyproject.toml +1 -1
  14. lean_runtime-2.1.2/scripts/announcement_gate.py +188 -0
  15. lean_runtime-2.1.2/scripts/registry_preflight.py +156 -0
  16. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_bundles.py +51 -0
  17. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_references.py +16 -0
  18. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/LICENSE +0 -0
  19. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/MANIFEST.in +0 -0
  20. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/README.md +0 -0
  21. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/environments.toml +0 -0
  22. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/core-v4.32.2.lock.json +0 -0
  23. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  24. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  25. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  26. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/README.md +0 -0
  27. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/mathlib-4.32.2.json +0 -0
  28. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/mathlib-4.32.2.toml +0 -0
  29. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/architecture.md +0 -0
  30. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/captures.md +0 -0
  31. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/case-study-v1.md +0 -0
  32. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/cli.md +0 -0
  33. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/environments.md +0 -0
  34. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/getting-started.md +0 -0
  35. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/index.md +0 -0
  36. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/local-projects.md +0 -0
  37. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/portable-copies.md +0 -0
  38. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/python-api.md +0 -0
  39. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/ready-programs.md +0 -0
  40. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/standalone-files.md +0 -0
  41. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/trust-and-limitations.md +0 -0
  42. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/v1-precision.md +0 -0
  43. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/examples/mathlib.toml +0 -0
  44. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/__init__.py +0 -0
  45. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/__main__.py +0 -0
  46. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/_git.py +0 -0
  47. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/_paths.py +0 -0
  48. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/backends.py +0 -0
  49. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/bundles.py +0 -0
  50. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/comparison.py +0 -0
  51. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/decisions.py +0 -0
  52. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/diagnostics.py +0 -0
  53. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/__init__.py +0 -0
  54. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/analyzer.py +0 -0
  55. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/api.py +0 -0
  56. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/candidate.py +0 -0
  57. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog.py +0 -0
  58. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_build.py +0 -0
  59. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_cli.py +0 -0
  60. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_manifest.py +0 -0
  61. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/data/catalog.json +0 -0
  62. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/defaults.py +0 -0
  63. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/errors.py +0 -0
  64. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/module_inventory.py +0 -0
  65. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/planner.py +0 -0
  66. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/policy.py +0 -0
  67. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/probe.py +0 -0
  68. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/py.typed +0 -0
  69. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/result.py +0 -0
  70. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schema_resources.py +0 -0
  71. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  72. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  73. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  74. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/errors.py +0 -0
  75. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/events.py +0 -0
  76. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/facade.py +0 -0
  77. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/frontmatter.py +0 -0
  78. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/health.py +0 -0
  79. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/lake.py +0 -0
  80. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/lockfiles.py +0 -0
  81. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/locking.py +0 -0
  82. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/matrix.py +0 -0
  83. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/models.py +0 -0
  84. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/policies.py +0 -0
  85. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/profiling.py +0 -0
  86. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/programs.py +0 -0
  87. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/projects.py +0 -0
  88. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/publisher_verification.py +0 -0
  89. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/py.typed +0 -0
  90. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/resolver.py +0 -0
  91. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/schema_resources.py +0 -0
  92. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/serialization.py +0 -0
  93. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/specs.py +0 -0
  94. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/store.py +0 -0
  95. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/timings.py +0 -0
  96. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/toolchain_slim.py +0 -0
  97. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/toolchains.py +0 -0
  98. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/verification.py +0 -0
  99. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/wire.py +0 -0
  100. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
  101. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/entry_points.txt +0 -0
  102. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/requires.txt +0 -0
  103. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/top_level.txt +0 -0
  104. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/mkdocs.yml +0 -0
  105. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/cleanup-v1.schema.json +0 -0
  106. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/comparison-v1.schema.json +0 -0
  107. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/execution-v1.schema.json +0 -0
  108. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/inspect-v1.schema.json +0 -0
  109. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/matrix-v1.schema.json +0 -0
  110. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/profile-v1.schema.json +0 -0
  111. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/verify-v1.schema.json +0 -0
  112. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/run_compatibility.py +0 -0
  113. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/run_v1_case_study.py +0 -0
  114. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/smoke_wheel.py +0 -0
  115. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/setup.cfg +0 -0
  116. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/conftest.py +0 -0
  117. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/e2e/test_real_discovery.py +0 -0
  118. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_analyzer.py +0 -0
  119. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_catalog.py +0 -0
  120. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_catalog_build.py +0 -0
  121. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_discovery_schema_resources.py +0 -0
  122. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_engine.py +0 -0
  123. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_planner.py +0 -0
  124. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_policy.py +0 -0
  125. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_backend.py +0 -0
  126. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_cli.py +0 -0
  127. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_diagnostics.py +0 -0
  128. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_environment_integration.py +0 -0
  129. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_events_health.py +0 -0
  130. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_facade.py +0 -0
  131. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_frontmatter.py +0 -0
  132. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_git.py +0 -0
  133. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_interactive.py +0 -0
  134. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_lockfiles.py +0 -0
  135. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_models.py +0 -0
  136. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_multifile.py +0 -0
  137. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_paths.py +0 -0
  138. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_prebuilt_policy.py +0 -0
  139. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_programs.py +0 -0
  140. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_projects.py +0 -0
  141. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_run_cli.py +0 -0
  142. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_runtime.py +0 -0
  143. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_schema_resources.py +0 -0
  144. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_schemas.py +0 -0
  145. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_signatures.py +0 -0
  146. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_specs.py +0 -0
  147. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_store.py +0 -0
  148. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_toolchain_slim.py +0 -0
  149. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_toolchains.py +0 -0
  150. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_v1_precision.py +0 -0
  151. {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_verification_inventory.py +0 -0
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.2 - 2026-08-12
4
+
5
+ - Raise `lean-run`'s default `--check-timeout` from 120 to 300 seconds: a
6
+ first Mathlib check legitimately takes 2-5 minutes on slower machines
7
+ today (instance staging plus Lake's full trace scan), and the old default
8
+ timed out authoritative checks that would have succeeded.
9
+
10
+ ## 2.1.1 - 2026-08-12
11
+
12
+ - The discovery search budget now gates starting additional candidates and
13
+ no longer kills an in-flight authoritative check, which is bounded by its
14
+ own per-candidate timeout. A slow machine's first Mathlib check can outlive
15
+ the remaining search budget legitimately.
16
+ - OCI blob downloads keep partials on truncated transfers and resume with a
17
+ Range request, retry transport errors mid-stream, and attempt four times.
18
+ - Add `build-and-publish --accelerate`: hydrate artifacts from known package
19
+ caches (Mathlib's `lake exe cache get`) for locked packages that carry no
20
+ artifact command of their own. Acceleration is keyed by exact canonical
21
+ package URL, never changes the lock identity, is recorded in the hydration
22
+ report, and the built environment is still probe-verified before
23
+ publication. The public-cache workflow uses it, turning multi-hour Mathlib
24
+ source builds into minutes.
25
+ - Add a fail-closed release toolkit: `scripts/registry_preflight.py` verifies
26
+ every bundled catalog entry is anonymously downloadable, and the
27
+ announcement-gate workflow runs the advertised PyPI journeys on clean
28
+ unauthenticated Linux and macOS runners (dispatch + daily canary).
29
+ - `lean-run` now warns loudly when a downloadable environment is unavailable
30
+ and states that source builds can take 30+ minutes, with a
31
+ `--no-source-build` hint, instead of falling back silently.
32
+
3
33
  ## 2.1.0 - 2026-08-11
4
34
 
5
35
  - Add `lean-runtime toolchain-slim` and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -78,3 +78,26 @@ a normal publisher and creates the PyPI project.
78
78
  PyPI does not permit replacing files for an already published version. If a
79
79
  release needs a correction, increment the version rather than recreating the
80
80
  tag.
81
+
82
+ ## Release and announcement protocol
83
+
84
+ Catalog entries are public claims that a prebuilt environment is
85
+ downloadable. The protocol below makes those claims fail-closed:
86
+
87
+ 1. Publish downloadable environments for every catalog entry and platform
88
+ (`public-cache.yml`) **before** shipping a wheel that references them, and
89
+ make the GHCR package publicly pullable.
90
+ 2. Run `python scripts/registry_preflight.py` — it verifies, anonymously,
91
+ that every catalog lock resolves to an index, platform manifests, and
92
+ readable blobs. CI-cheap; run it on every release.
93
+ 3. After publishing to PyPI, dispatch the **Announcement gate** workflow
94
+ (`announcement-gate.yml`). It installs from PyPI on clean unauthenticated
95
+ Linux and macOS runners and runs the advertised journeys from
96
+ `acceptance/`: a cold check with `--no-source-build` (fail-closed: a
97
+ broken registry fails in seconds instead of hiding behind a source
98
+ build), a warm re-check, a failing proof with logical diagnostic paths, a
99
+ lock round-trip, and the Python batch API. Release titles use `vX.Y.Z`,
100
+ matching the tag.
101
+ 4. Announce only after the gate is green. The same workflow runs daily as a
102
+ canary: registry permissions and docs deployments can break without any
103
+ code change.
@@ -153,6 +153,13 @@ def parser() -> argparse.ArgumentParser:
153
153
  action="store_true",
154
154
  help="publish blobs and platform manifest without updating the lock index",
155
155
  )
156
+ push.add_argument(
157
+ "--accelerate",
158
+ action="store_true",
159
+ help="hydrate artifacts from known package caches (e.g. Mathlib's) for locked "
160
+ "packages without an artifact command; the lock identity is unchanged and the "
161
+ "built environment is still probe-verified before publication",
162
+ )
156
163
  push.add_argument("--sign", action="store_true", help="sign the published lock index")
157
164
  push.add_argument(
158
165
  "--attest", action="store_true", help="publish a signed source/probe attestation"
@@ -391,6 +398,7 @@ def main(argv: list[str] | None = None) -> int:
391
398
  EnvironmentLock.load(args.lock),
392
399
  name=args.name,
393
400
  build_timeout=args.timeout,
401
+ accelerate=args.accelerate,
394
402
  )
395
403
  _json(
396
404
  runtime.publish_environment(
@@ -223,10 +223,11 @@ def discover(
223
223
  else attempt.diagnostics,
224
224
  )
225
225
  remaining = deadline - time.monotonic()
226
- timeout = min(
227
- remaining,
228
- plan.policy.candidate_timeout_seconds or remaining,
229
- )
226
+ # The search budget gates STARTING candidates (checked at the top of
227
+ # the loop); it must not kill an in-flight authoritative check, which
228
+ # is bounded by its own per-candidate timeout. A slow machine's first
229
+ # check may legitimately outlive the remaining search budget.
230
+ timeout = plan.policy.candidate_timeout_seconds or remaining
230
231
  with _AttemptCancellation(timeout, cancel) as attempt_cancel:
231
232
  try:
232
233
  outcome = probe.check(
@@ -35,6 +35,7 @@ from .models import (
35
35
  PhaseTiming,
36
36
  )
37
37
  from .policies import ExecutionPolicy
38
+ from .references import artifact_accelerators
38
39
  from .serialization import sha256_id, write_json_atomic
39
40
  from .store import (
40
41
  EnvironmentStore,
@@ -1011,6 +1012,7 @@ class EnvironmentManager:
1011
1012
  name: str | None = None,
1012
1013
  build_profile: str = "release",
1013
1014
  build_timeout: float = 1800,
1015
+ accelerate: bool = False,
1014
1016
  cancel: threading.Event | None = None,
1015
1017
  ) -> Environment:
1016
1018
  self.store.publish_lock(lock)
@@ -1030,6 +1032,7 @@ class EnvironmentManager:
1030
1032
  destination,
1031
1033
  build_profile,
1032
1034
  build_timeout=build_timeout,
1035
+ accelerate=accelerate,
1033
1036
  cancel=cancel,
1034
1037
  )
1035
1038
  else:
@@ -1075,6 +1078,7 @@ class EnvironmentManager:
1075
1078
  build_profile: str,
1076
1079
  *,
1077
1080
  build_timeout: float,
1081
+ accelerate: bool = False,
1078
1082
  cancel: threading.Event | None,
1079
1083
  ) -> None:
1080
1084
  stage = self.store.environments / f".staging-{os.getpid()}-{uuid.uuid4().hex}"
@@ -1105,15 +1109,20 @@ class EnvironmentManager:
1105
1109
  max_output_bytes=10_000_000,
1106
1110
  )
1107
1111
  hydration: list[dict[str, Any]] = []
1112
+ accelerators = artifact_accelerators() if accelerate else {}
1108
1113
  for package in lock.packages:
1109
- if not package.artifact_command:
1114
+ requested_command = package.artifact_command or accelerators.get(package.url, ())
1115
+ if not requested_command:
1110
1116
  continue
1117
+ accelerated = not package.artifact_command
1111
1118
  self.events.emit(
1112
1119
  "artifact.hydration_started",
1113
- f"Hydrating artifacts for {package.name}",
1120
+ f"Hydrating artifacts for {package.name}"
1121
+ + (" (accelerated)" if accelerated else ""),
1114
1122
  package=package.name,
1123
+ accelerated=accelerated,
1115
1124
  )
1116
- command = list(package.artifact_command)
1125
+ command = list(requested_command)
1117
1126
  if command[0] in {"lake", "lean"}:
1118
1127
  command = self.toolchains.command(lock.toolchain, command[0], *command[1:])
1119
1128
  result = self.backend.execute(
@@ -1127,6 +1136,7 @@ class EnvironmentManager:
1127
1136
  {
1128
1137
  "package": package.name,
1129
1138
  "command": command,
1139
+ "accelerated": accelerated,
1130
1140
  "exit_code": result.exit_code,
1131
1141
  "output": result.stdout + result.stderr,
1132
1142
  }
@@ -32,7 +32,7 @@ _BEARER_PARAMETER = re.compile(r'([a-zA-Z]+)="([^"]*)"')
32
32
  _DIGEST = re.compile(r"sha256:([0-9a-f]{64})")
33
33
  _ACCEPT = ", ".join((INDEX_MEDIA_TYPE, MANIFEST_MEDIA_TYPE))
34
34
  DEFAULT_ENVIRONMENT_LIBRARIES = ("oci://ghcr.io/alerad/lean-runtime-cache",)
35
- _BLOB_INTEGRITY_ATTEMPTS = 2
35
+ _BLOB_INTEGRITY_ATTEMPTS = 4
36
36
 
37
37
 
38
38
  class SignatureVerifier(Protocol):
@@ -263,18 +263,30 @@ class OCIRegistryClient:
263
263
  if written == size and observed_digest == digest:
264
264
  temporary.replace(destination)
265
265
  break
266
- temporary.unlink(missing_ok=True)
266
+ # A short read is a truncated transfer (registry or proxy
267
+ # closed the connection early): keep the partial so the
268
+ # next attempt resumes with a Range request instead of
269
+ # restarting a multi-gigabyte download from zero. Only a
270
+ # complete-but-mismatched blob is real corruption.
271
+ truncated = written < size
272
+ if not truncated:
273
+ temporary.unlink(missing_ok=True)
267
274
  if attempt == _BLOB_INTEGRITY_ATTEMPTS:
275
+ temporary.unlink(missing_ok=True)
276
+ reason = "was truncated" if truncated else "failed digest verification"
268
277
  raise EnvironmentError(
269
- "downloaded OCI blob failed digest verification "
278
+ f"downloaded OCI blob {reason} "
270
279
  f"after {attempt} attempts (expected {digest}, got "
271
280
  f"{observed_digest}, bytes {written}/{size})"
272
281
  )
273
282
  events.emit(
274
283
  "library.layer_download_retry",
275
- "Retrying OCI blob after integrity verification failed",
284
+ "Resuming truncated OCI blob"
285
+ if truncated
286
+ else "Retrying OCI blob after integrity verification failed",
276
287
  digest=digest,
277
288
  attempt=attempt + 1,
289
+ truncated=truncated,
278
290
  observed_digest=observed_digest,
279
291
  downloaded_bytes=written,
280
292
  expected_bytes=size,
@@ -282,7 +294,17 @@ class OCIRegistryClient:
282
294
  except urllib.error.HTTPError as exc:
283
295
  raise DownloadUnavailable(f"OCI blob download failed: HTTP {exc.code}") from exc
284
296
  except OSError as exc:
285
- raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
297
+ # Connection resets and read timeouts mid-transfer are
298
+ # retryable; the kept partial resumes on the next attempt.
299
+ if attempt == _BLOB_INTEGRITY_ATTEMPTS:
300
+ raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
301
+ events.emit(
302
+ "library.layer_download_retry",
303
+ "Retrying OCI blob after a transport error",
304
+ digest=digest,
305
+ attempt=attempt + 1,
306
+ error=str(exc),
307
+ )
286
308
  return destination
287
309
 
288
310
  def blob_exists(self, digest: str) -> bool:
@@ -41,6 +41,20 @@ PACKAGE_ALIASES: dict[str, tuple[str, str, tuple[str, ...]]] = {
41
41
  }
42
42
 
43
43
 
44
+ def artifact_accelerators() -> dict[str, tuple[str, ...]]:
45
+ """Map canonical package URLs to their known artifact-cache commands.
46
+
47
+ Used by opt-in build acceleration for locks whose packages carry no
48
+ ``artifact_command`` of their own. Keyed by exact URL so a fork that
49
+ merely shares a package name is never accelerated.
50
+ """
51
+ return {
52
+ f"https://github.com/{owner}/{repository}.git": command
53
+ for owner, repository, command in PACKAGE_ALIASES.values()
54
+ if command
55
+ }
56
+
57
+
44
58
  @dataclass(frozen=True, slots=True)
45
59
  class PackageReference:
46
60
  """A human-friendly reference to one Git-hosted Lean package."""
@@ -36,9 +36,18 @@ def _progress(event: RuntimeEvent) -> None:
36
36
  "library.layer_download_started": "Downloading cached environment",
37
37
  "library.layer_download_retry": "Retrying cached-environment download",
38
38
  "toolchain.install_started": "Installing Lean toolchain (a first run can take minutes)",
39
- "environment.build_started": "Building environment",
39
+ "environment.build_started": "Building environment from source (large dependencies "
40
+ "such as Mathlib can take 30+ minutes; pass --no-source-build to fail fast instead)",
40
41
  "environment.cache_hit": "Using local environment",
41
42
  }
43
+ if event.kind == "availability.fallback":
44
+ library = event.data.get("library", "environment library")
45
+ reason = event.data.get("reason_code") or event.data.get("reason") or "unavailable"
46
+ print(
47
+ f"lean-run: WARNING: downloadable environment unavailable from {library} ({reason})",
48
+ file=sys.stderr,
49
+ )
50
+ return
42
51
  message = messages.get(event.kind)
43
52
  if message:
44
53
  package = f" {event.data['reference']}" if "reference" in event.data else ""
@@ -97,7 +106,7 @@ def parser() -> argparse.ArgumentParser:
97
106
  "--timeout",
98
107
  dest="check_timeout",
99
108
  type=float,
100
- default=120,
109
+ default=300,
101
110
  help="budget for one Lean invocation (--timeout is a deprecated alias)",
102
111
  )
103
112
  return root
@@ -171,6 +171,7 @@ class Runtime:
171
171
  name: str | None = None,
172
172
  build_profile: str = "release",
173
173
  build_timeout: float = 1800,
174
+ accelerate: bool = False,
174
175
  cancel: threading.Event | None = None,
175
176
  ) -> Environment:
176
177
  environment_id = environment_identity(lock, build_profile)
@@ -209,6 +210,7 @@ class Runtime:
209
210
  name=name,
210
211
  build_profile=build_profile,
211
212
  build_timeout=build_timeout,
213
+ accelerate=accelerate,
212
214
  cancel=cancel,
213
215
  )
214
216
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -104,6 +104,8 @@ schemas/inspect-v1.schema.json
104
104
  schemas/matrix-v1.schema.json
105
105
  schemas/profile-v1.schema.json
106
106
  schemas/verify-v1.schema.json
107
+ scripts/announcement_gate.py
108
+ scripts/registry_preflight.py
107
109
  scripts/run_compatibility.py
108
110
  scripts/run_v1_case_study.py
109
111
  scripts/smoke_wheel.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.1.0"
7
+ version = "2.1.2"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """Fail-closed announcement gate: the advertised user journeys, from clean state.
3
+
4
+ Runs the exact journeys a new user follows after ``pip install lean-runtime``
5
+ against an empty runtime home, with hard wall-clock timeouts, and writes one
6
+ JSON report. The critical property is fail-closed distribution: the first cold
7
+ check runs with ``--no-source-build``, so a broken or private registry fails
8
+ in seconds instead of degrading into a silent source build.
9
+
10
+ Steps:
11
+ 1. cold check of acceptance/Main.lean with --no-source-build and --lock-out
12
+ 2. warm re-check (must be fast)
13
+ 3. acceptance/Fail.lean must be rejected with a diagnostic naming Fail.lean
14
+ 4. re-check via the emitted lock (reproducibility)
15
+ 5. the documented Python batch API against the same lock
16
+
17
+ Run it on a machine WITHOUT registry credentials; the point is proving the
18
+ anonymous path. The runtime home is created fresh; pass --keep-home to retain
19
+ it for inspection.
20
+
21
+ Usage:
22
+ python scripts/announcement_gate.py --report gate-report.json
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ import time
33
+ from pathlib import Path
34
+
35
+ ACCEPTANCE = Path(__file__).resolve().parent.parent / "acceptance"
36
+
37
+ COLD_TIMEOUT = 2700 # toolchain download + environment download, no builds
38
+ WARM_TIMEOUT = 300
39
+ WARM_LIMIT = 240 # warm Mathlib checks include a full Lake trace scan today
40
+
41
+
42
+ def run(
43
+ command: list[str],
44
+ *,
45
+ timeout: float,
46
+ env: dict[str, str],
47
+ ) -> tuple[int, str, float]:
48
+ started = time.monotonic()
49
+ try:
50
+ completed = subprocess.run(
51
+ command,
52
+ capture_output=True,
53
+ text=True,
54
+ timeout=timeout,
55
+ env=env,
56
+ check=False,
57
+ )
58
+ output = completed.stdout + completed.stderr
59
+ return completed.returncode, output, time.monotonic() - started
60
+ except subprocess.TimeoutExpired as error:
61
+ output = (error.stdout or "") + (error.stderr or "")
62
+ if isinstance(output, bytes): # pragma: no cover - platform dependent
63
+ output = output.decode(errors="replace")
64
+ return -1, f"TIMEOUT after {timeout}s\n{output}", time.monotonic() - started
65
+
66
+
67
+ def main() -> int:
68
+ parser = argparse.ArgumentParser(description=__doc__)
69
+ parser.add_argument("--home", default=None, help="runtime home (default: fresh temp dir)")
70
+ parser.add_argument("--report", default="announcement-gate-report.json")
71
+ parser.add_argument("--keep-home", action="store_true")
72
+ parser.add_argument("--cold-timeout", type=float, default=COLD_TIMEOUT)
73
+ arguments = parser.parse_args()
74
+
75
+ import os
76
+ import tempfile
77
+
78
+ home = Path(arguments.home) if arguments.home else Path(tempfile.mkdtemp(prefix="gate-home-"))
79
+ home.mkdir(parents=True, exist_ok=True)
80
+ if any(home.iterdir()):
81
+ print(f"refusing to run: home is not empty: {home}")
82
+ return 2
83
+ env = {**os.environ, "LEAN_RUNTIME_HOME": str(home)}
84
+ lock_path = home / "gate.lock.json"
85
+ main_lean = str(ACCEPTANCE / "Main.lean")
86
+ fail_lean = str(ACCEPTANCE / "Fail.lean")
87
+
88
+ report: dict[str, object] = {"home": str(home), "steps": [], "ok": False}
89
+ steps: list[dict[str, object]] = report["steps"] # type: ignore[assignment]
90
+
91
+ def step(
92
+ name: str,
93
+ command: list[str],
94
+ *,
95
+ timeout: float,
96
+ expect_exit: int = 0,
97
+ require: str | None = None,
98
+ forbid: str | None = None,
99
+ limit: float | None = None,
100
+ ) -> bool:
101
+ code, output, elapsed = run(command, timeout=timeout, env=env)
102
+ ok = code == expect_exit
103
+ detail = ""
104
+ if ok and require is not None and require not in output:
105
+ ok, detail = False, f"missing expected text: {require!r}"
106
+ if ok and forbid is not None and forbid in output:
107
+ ok, detail = False, f"contains forbidden text: {forbid!r}"
108
+ if ok and limit is not None and elapsed > limit:
109
+ ok, detail = False, f"took {elapsed:.1f}s, limit {limit}s"
110
+ steps.append(
111
+ {
112
+ "name": name,
113
+ "command": command,
114
+ "exit_code": code,
115
+ "elapsed_seconds": round(elapsed, 2),
116
+ "ok": ok,
117
+ "detail": detail or None,
118
+ "tail": output[-2000:],
119
+ }
120
+ )
121
+ suffix = f": {detail}" if detail else ""
122
+ print(f"{'ok ' if ok else 'FAIL'} {name} ({elapsed:.1f}s){suffix}")
123
+ return ok
124
+
125
+ version = subprocess.run(
126
+ [
127
+ sys.executable,
128
+ "-c",
129
+ "import lean_runtime, importlib.metadata as m; print(m.version('lean-runtime'))",
130
+ ],
131
+ capture_output=True,
132
+ text=True,
133
+ check=False,
134
+ ).stdout.strip()
135
+ report["lean_runtime_version"] = version
136
+ print(f"gate: lean-runtime {version}, home {home}")
137
+
138
+ ok = step(
139
+ "cold-no-source-build",
140
+ [
141
+ "lean-run",
142
+ main_lean,
143
+ "--no-source-build",
144
+ "--lock-out",
145
+ str(lock_path),
146
+ ],
147
+ timeout=arguments.cold_timeout,
148
+ )
149
+ ok = ok and step(
150
+ "warm",
151
+ ["lean-run", main_lean],
152
+ timeout=WARM_TIMEOUT,
153
+ limit=WARM_LIMIT,
154
+ )
155
+ ok = ok and step(
156
+ "failing-proof-diagnostics",
157
+ ["lean-run", fail_lean, "--lock", str(lock_path)],
158
+ timeout=WARM_TIMEOUT,
159
+ expect_exit=1,
160
+ require="Fail.lean",
161
+ forbid="instance-",
162
+ )
163
+ ok = ok and step(
164
+ "lock-reproducibility",
165
+ ["lean-run", main_lean, "--lock", str(lock_path)],
166
+ timeout=WARM_TIMEOUT,
167
+ limit=WARM_LIMIT,
168
+ )
169
+ ok = ok and step(
170
+ "python-batch-api",
171
+ [sys.executable, str(ACCEPTANCE / "python_api.py"), str(lock_path)],
172
+ timeout=900,
173
+ )
174
+
175
+ usage = shutil.disk_usage(home)
176
+ report["ok"] = ok
177
+ report["home_bytes"] = sum(f.stat().st_size for f in home.rglob("*") if f.is_file())
178
+ report["disk_free_bytes"] = usage.free
179
+ Path(arguments.report).write_text(json.dumps(report, indent=2), encoding="utf-8")
180
+ print(f"report: {arguments.report}")
181
+ if not arguments.keep_home and arguments.home is None:
182
+ shutil.rmtree(home, ignore_errors=True)
183
+ print("GATE PASSED" if ok else "GATE FAILED")
184
+ return 0 if ok else 1
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())