lean-runtime 2.3.0__tar.gz → 2.4.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 (163) hide show
  1. {lean_runtime-2.3.0/lean_runtime.egg-info → lean_runtime-2.4.0}/PKG-INFO +1 -1
  2. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/__init__.py +2 -0
  3. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/cli.py +129 -15
  4. lean_runtime-2.4.0/lean_runtime/console.py +246 -0
  5. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/probe.py +8 -0
  6. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/errors.py +9 -0
  7. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/events.py +28 -3
  8. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/oci.py +86 -6
  9. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/policies.py +42 -0
  10. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/run_cli.py +123 -33
  11. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/runtime.py +62 -1
  12. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/store.py +119 -4
  13. {lean_runtime-2.3.0 → lean_runtime-2.4.0/lean_runtime.egg-info}/PKG-INFO +1 -1
  14. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/SOURCES.txt +2 -0
  15. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/pyproject.toml +1 -1
  16. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_bundles.py +110 -1
  17. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_cli.py +54 -0
  18. lean_runtime-2.4.0/tests/test_console.py +251 -0
  19. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_run_cli.py +142 -1
  20. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_store.py +46 -0
  21. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/CHANGELOG.md +0 -0
  22. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/LICENSE +0 -0
  23. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/MANIFEST.in +0 -0
  24. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/README.md +0 -0
  25. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/environments.toml +0 -0
  26. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  27. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  28. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  29. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  30. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  31. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  32. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  33. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  34. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  35. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/README.md +0 -0
  36. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.32.2.json +0 -0
  37. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.32.2.toml +0 -0
  38. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.33.0.json +0 -0
  39. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/compatibility/mathlib-4.33.0.toml +0 -0
  40. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/architecture.md +0 -0
  41. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/captures.md +0 -0
  42. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/case-study-v1.md +0 -0
  43. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/cli.md +0 -0
  44. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/development.md +0 -0
  45. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/environments.md +0 -0
  46. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/getting-started.md +0 -0
  47. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/index.md +0 -0
  48. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/local-projects.md +0 -0
  49. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/portable-copies.md +0 -0
  50. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/project-publishing.md +0 -0
  51. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/python-api.md +0 -0
  52. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/ready-programs.md +0 -0
  53. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/standalone-files.md +0 -0
  54. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/trust-and-limitations.md +0 -0
  55. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/docs/v1-precision.md +0 -0
  56. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/examples/mathlib.toml +0 -0
  57. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/__main__.py +0 -0
  58. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/_git.py +0 -0
  59. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/_paths.py +0 -0
  60. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/backends.py +0 -0
  61. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/bundles.py +0 -0
  62. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/comparison.py +0 -0
  63. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/decisions.py +0 -0
  64. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/diagnostics.py +0 -0
  65. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/__init__.py +0 -0
  66. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/analyzer.py +0 -0
  67. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/api.py +0 -0
  68. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/candidate.py +0 -0
  69. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog.py +0 -0
  70. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_build.py +0 -0
  71. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  72. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  73. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/data/catalog.json +0 -0
  74. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/defaults.py +0 -0
  75. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/engine.py +0 -0
  76. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/errors.py +0 -0
  77. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/module_inventory.py +0 -0
  78. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/planner.py +0 -0
  79. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/policy.py +0 -0
  80. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/py.typed +0 -0
  81. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/result.py +0 -0
  82. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schema_resources.py +0 -0
  83. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  84. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  85. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  86. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/environments.py +0 -0
  87. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/facade.py +0 -0
  88. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/frontmatter.py +0 -0
  89. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/health.py +0 -0
  90. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/lake.py +0 -0
  91. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/lockfiles.py +0 -0
  92. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/locking.py +0 -0
  93. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/matrix.py +0 -0
  94. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/models.py +0 -0
  95. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/profiling.py +0 -0
  96. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/programs.py +0 -0
  97. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/projects.py +0 -0
  98. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/publisher_verification.py +0 -0
  99. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/py.typed +0 -0
  100. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/references.py +0 -0
  101. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/resolver.py +0 -0
  102. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/schema_resources.py +0 -0
  103. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/serialization.py +0 -0
  104. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/specs.py +0 -0
  105. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/timings.py +0 -0
  106. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/toolchain_slim.py +0 -0
  107. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/toolchains.py +0 -0
  108. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/verification.py +0 -0
  109. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime/wire.py +0 -0
  110. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  111. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  112. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/requires.txt +0 -0
  113. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/lean_runtime.egg-info/top_level.txt +0 -0
  114. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/mkdocs.yml +0 -0
  115. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/cleanup-v1.schema.json +0 -0
  116. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/comparison-v1.schema.json +0 -0
  117. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/execution-v1.schema.json +0 -0
  118. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/inspect-v1.schema.json +0 -0
  119. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/matrix-v1.schema.json +0 -0
  120. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/profile-v1.schema.json +0 -0
  121. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/schemas/verify-v1.schema.json +0 -0
  122. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/announcement_gate.py +0 -0
  123. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/registry_preflight.py +0 -0
  124. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/run_compatibility.py +0 -0
  125. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/run_v1_case_study.py +0 -0
  126. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/scripts/smoke_wheel.py +0 -0
  127. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/setup.cfg +0 -0
  128. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/conftest.py +0 -0
  129. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  130. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_analyzer.py +0 -0
  131. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_catalog.py +0 -0
  132. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_catalog_build.py +0 -0
  133. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  134. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_engine.py +0 -0
  135. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_planner.py +0 -0
  136. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/discovery/test_policy.py +0 -0
  137. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_backend.py +0 -0
  138. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_diagnostics.py +0 -0
  139. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_environment_integration.py +0 -0
  140. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_events_health.py +0 -0
  141. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_facade.py +0 -0
  142. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_frontmatter.py +0 -0
  143. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_git.py +0 -0
  144. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_interactive.py +0 -0
  145. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_lockfiles.py +0 -0
  146. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_models.py +0 -0
  147. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_multifile.py +0 -0
  148. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_paths.py +0 -0
  149. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_prebuilt_policy.py +0 -0
  150. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_programs.py +0 -0
  151. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_project_publish.py +0 -0
  152. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_projects.py +0 -0
  153. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_references.py +0 -0
  154. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_runtime.py +0 -0
  155. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_schema_resources.py +0 -0
  156. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_schemas.py +0 -0
  157. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_signatures.py +0 -0
  158. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_specs.py +0 -0
  159. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_toolchain_slim.py +0 -0
  160. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_toolchains.py +0 -0
  161. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_v1_precision.py +0 -0
  162. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_verification_inventory.py +0 -0
  163. {lean_runtime-2.3.0 → lean_runtime-2.4.0}/tests/test_verification_probe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -13,6 +13,7 @@ from .environments import (
13
13
  InteractiveSession,
14
14
  )
15
15
  from .errors import (
16
+ DownloadLimitExceeded,
16
17
  DownloadUnavailable,
17
18
  EnvironmentError,
18
19
  LeanCheckError,
@@ -76,6 +77,7 @@ __all__ = [
76
77
  "ComparisonEntry",
77
78
  "PortableCopyInfo",
78
79
  "DownloadCleanupReport",
80
+ "DownloadLimitExceeded",
79
81
  "DEFAULT_ENVIRONMENT_LIBRARIES",
80
82
  "DependencyInput",
81
83
  "DiscoveredPackage",
@@ -6,19 +6,22 @@ import argparse
6
6
  import json
7
7
  import sys
8
8
  import time
9
+ from datetime import datetime
9
10
  from pathlib import Path
10
11
  from typing import Any
11
12
 
13
+ from .console import styler_for
12
14
  from .environments import ExecutionCapture
13
15
  from .errors import LeanRuntimeError, MaterializationError, ResolutionError
14
16
  from .events import RuntimeEvent
15
17
  from .lockfiles import EnvironmentLock
16
18
  from .matrix import load_matrix
17
19
  from .models import ExecutionResult, PhaseTiming
18
- from .policies import ExecutionPolicy
20
+ from .policies import ExecutionPolicy, format_byte_size
19
21
  from .projects import discover_project, project_publication_workflow
20
22
  from .runtime import Runtime
21
23
  from .specs import EnvironmentSpec
24
+ from .store import CleanupReport, DownloadCleanupReport, StoreStatus
22
25
  from .timings import render_timings
23
26
  from .wire import (
24
27
  envelope,
@@ -46,6 +49,105 @@ def _json(value: Any) -> None:
46
49
  print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
47
50
 
48
51
 
52
+ def _relative_age(timestamp: str | None) -> str:
53
+ if not timestamp:
54
+ return "unknown"
55
+ try:
56
+ moment = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
57
+ except ValueError:
58
+ return "unknown"
59
+ seconds = max(0.0, time.time() - moment.timestamp())
60
+ for unit_seconds, label in ((31_536_000, "y"), (2_592_000, "mo"), (86_400, "d"), (3_600, "h")):
61
+ if seconds >= unit_seconds:
62
+ return f"{int(seconds // unit_seconds)}{label} ago"
63
+ return "recent"
64
+
65
+
66
+ def _render_storage(status: StoreStatus) -> None:
67
+ style = styler_for(sys.stdout)
68
+ print(f"{style.bold('Store')} {status.home}")
69
+ print()
70
+ rows = (
71
+ ("Environments", status.environments, status.environments_bytes),
72
+ ("Sources", status.sources, status.sources_bytes),
73
+ ("Download cache", status.oci_blobs, status.oci_blobs_bytes),
74
+ ("Toolchains", None, status.toolchains_bytes),
75
+ ("Executions", status.executions, status.executions_bytes),
76
+ )
77
+ for label, count, bytes_used in rows:
78
+ counted = f"{count:>5}" if count is not None else " "
79
+ size_column = style.cyan(f"{format_byte_size(bytes_used):>10}")
80
+ print(f" {label:<15}{counted} {size_column}")
81
+ total_label = style.bold(f"{'Total':<15}")
82
+ total_size = style.bold(f"{format_byte_size(status.bytes_used):>10}")
83
+ free_note = style.dim(f"({format_byte_size(status.bytes_free)} free on disk)")
84
+ print(f" {total_label} {total_size} {free_note}")
85
+ if status.environment_usage:
86
+ print()
87
+ print(style.bold("Largest environments"))
88
+ for usage in status.environment_usage[:5]:
89
+ name = ", ".join(usage.aliases) if usage.aliases else usage.environment_id[:16] + "…"
90
+ details = " · ".join(
91
+ part
92
+ for part in (usage.toolchain, f"last used {_relative_age(usage.last_used_at)}")
93
+ if part
94
+ )
95
+ size_column = style.cyan(f"{format_byte_size(usage.bytes_used):>10}")
96
+ print(f" {size_column} {name} {style.dim(details)}")
97
+ print()
98
+ print(style.dim("Reclaim space: lean-runtime clean (preview, keeps recent/named)"))
99
+ print(style.dim(" lean-runtime clean --execute --include-downloads"))
100
+
101
+
102
+ def _render_cleanup(environments: CleanupReport, downloads: DownloadCleanupReport | None) -> None:
103
+ style = styler_for(sys.stdout)
104
+ retained_note = style.dim(
105
+ f"Retained {len(environments.retained)} environment(s) (named, recent, or in use)."
106
+ )
107
+ if environments.dry_run:
108
+ anything = False
109
+ if environments.candidates:
110
+ anything = True
111
+ print(
112
+ style.bold(
113
+ f"Would remove {len(environments.candidates)} unused environment(s) · "
114
+ f"{format_byte_size(environments.candidate_bytes)}"
115
+ )
116
+ )
117
+ for name in environments.candidates:
118
+ print(f" {style.dim(name)}")
119
+ else:
120
+ print("No unused environments to remove.")
121
+ if downloads is not None:
122
+ if downloads.candidates:
123
+ anything = True
124
+ print(
125
+ style.bold(
126
+ f"Would remove {len(downloads.candidates)} cached download(s) · "
127
+ f"{format_byte_size(downloads.candidate_bytes)}"
128
+ )
129
+ )
130
+ else:
131
+ print("No unreferenced cached downloads to remove.")
132
+ print(retained_note)
133
+ if anything:
134
+ print()
135
+ print("This was a preview. Re-run with --execute to delete.")
136
+ return
137
+ reclaimed = environments.reclaimed_bytes + (downloads.reclaimed_bytes if downloads else 0)
138
+ removed = len(environments.removed) + (len(downloads.removed) if downloads else 0)
139
+ if removed:
140
+ parts = [f"{len(environments.removed)} environment(s)"]
141
+ if downloads is not None:
142
+ parts.append(f"{len(downloads.removed)} cached download(s)")
143
+ print(
144
+ style.green(f"Removed {' and '.join(parts)} · reclaimed {format_byte_size(reclaimed)}")
145
+ )
146
+ else:
147
+ print("Nothing needed cleaning.")
148
+ print(retained_note)
149
+
150
+
49
151
  def _progress(event: RuntimeEvent) -> None:
50
152
  package = f" [{event.data['package']}]" if "package" in event.data else ""
51
153
  print(f"lean-runtime: {event.kind}{package}: {event.message}", file=sys.stderr)
@@ -266,7 +368,8 @@ def parser() -> argparse.ArgumentParser:
266
368
  inspect.add_argument("--explain", action="store_true", help="explain identity and reuse")
267
369
 
268
370
  commands.add_parser("environments", help="list ready environments")
269
- commands.add_parser("storage", help="show downloaded and built storage usage")
371
+ storage = commands.add_parser("storage", help="show downloaded and built storage usage")
372
+ storage.add_argument("--json", action="store_true")
270
373
  commands.add_parser("doctor", help="check local prerequisites and environment storage")
271
374
 
272
375
  verify = commands.add_parser("verify", help="verify a lock or published environment")
@@ -316,6 +419,7 @@ def parser() -> argparse.ArgumentParser:
316
419
  gc.add_argument(
317
420
  "--include-downloads", action="store_true", help="also clean unused downloaded files"
318
421
  )
422
+ gc.add_argument("--json", action="store_true")
319
423
 
320
424
  raw = commands.add_parser(
321
425
  "check-file", help="check a file, discovering its local Lake project when possible"
@@ -640,7 +744,11 @@ def main(argv: list[str] | None = None) -> int:
640
744
  _json(list(runtime.list_environments()))
641
745
  return 0
642
746
  if args.command == "storage":
643
- _json(runtime.store_status().to_dict())
747
+ status = runtime.store_status()
748
+ if args.json:
749
+ _json(status.to_dict())
750
+ else:
751
+ _render_storage(status)
644
752
  return 0
645
753
  if args.command == "toolchain-slim":
646
754
  manifest = runtime.toolchains.materialize_slim(args.toolchain)
@@ -734,22 +842,28 @@ def main(argv: list[str] | None = None) -> int:
734
842
  dry_run=not args.execute,
735
843
  minimum_age_seconds=args.minimum_age_hours * 3600,
736
844
  )
737
- gc_payload: dict[str, Any] = {
738
- "environments": gc_report.to_dict(),
739
- "downloaded_files": None,
740
- }
741
- if args.include_downloads:
742
- gc_payload["downloaded_files"] = runtime.clean_downloads(
845
+ gc_downloads = (
846
+ runtime.clean_downloads(
743
847
  dry_run=not args.execute,
744
848
  minimum_age_seconds=args.minimum_age_hours * 3600,
745
- ).to_dict()
746
- _json(
747
- envelope(
748
- "lean-runtime.cleanup/v1",
749
- ok=True,
750
- data=gc_payload,
751
849
  )
850
+ if args.include_downloads
851
+ else None
752
852
  )
853
+ if args.json:
854
+ gc_payload: dict[str, Any] = {
855
+ "environments": gc_report.to_dict(),
856
+ "downloaded_files": gc_downloads.to_dict() if gc_downloads else None,
857
+ }
858
+ _json(
859
+ envelope(
860
+ "lean-runtime.cleanup/v1",
861
+ ok=True,
862
+ data=gc_payload,
863
+ )
864
+ )
865
+ else:
866
+ _render_cleanup(gc_report, gc_downloads)
753
867
  return 0
754
868
  if args.command == "check":
755
869
  if args.package_refs:
@@ -0,0 +1,246 @@
1
+ """Terminal rendering of structured runtime progress events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import time
8
+ from typing import Any, Literal, TextIO
9
+
10
+ from .events import RuntimeEvent
11
+ from .policies import format_byte_size
12
+
13
+ RenderMode = Literal["tty", "plain", "quiet"]
14
+
15
+ _BAR_WIDTH = 20
16
+ _TTY_REDRAW_SECONDS = 0.1
17
+ _PLAIN_CHECKPOINT_PERCENT = 25
18
+
19
+
20
+ def select_mode(*, quiet: bool = False, stream: TextIO | None = None) -> RenderMode:
21
+ if quiet:
22
+ return "quiet"
23
+ stream = stream if stream is not None else sys.stderr
24
+ return "tty" if stream.isatty() else "plain"
25
+
26
+
27
+ class Styler:
28
+ """Minimal ANSI styling that degrades to plain text when disabled."""
29
+
30
+ def __init__(self, enabled: bool) -> None:
31
+ self.enabled = enabled
32
+
33
+ def _wrap(self, code: str, text: str) -> str:
34
+ return f"\x1b[{code}m{text}\x1b[0m" if self.enabled else text
35
+
36
+ def bold(self, text: str) -> str:
37
+ return self._wrap("1", text)
38
+
39
+ def dim(self, text: str) -> str:
40
+ return self._wrap("2", text)
41
+
42
+ def red(self, text: str) -> str:
43
+ return self._wrap("31", text)
44
+
45
+ def green(self, text: str) -> str:
46
+ return self._wrap("32", text)
47
+
48
+ def yellow(self, text: str) -> str:
49
+ return self._wrap("33", text)
50
+
51
+ def cyan(self, text: str) -> str:
52
+ return self._wrap("36", text)
53
+
54
+
55
+ def styler_for(stream: TextIO | None = None) -> Styler:
56
+ """Style only real terminals, honoring the NO_COLOR convention."""
57
+ stream = stream if stream is not None else sys.stderr
58
+ isatty = getattr(stream, "isatty", None)
59
+ enabled = bool(isatty and isatty()) and "NO_COLOR" not in os.environ
60
+ return Styler(enabled)
61
+
62
+
63
+ class ConsoleRenderer:
64
+ """Render runtime events as terse, single-purpose progress output.
65
+
66
+ Warm runs stay silent; cold acquisitions get one context line and one
67
+ aggregate progress line. Everything else is behind ``verbose``.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ stream: TextIO | None = None,
73
+ *,
74
+ mode: RenderMode | None = None,
75
+ verbose: bool = False,
76
+ color: bool | None = None,
77
+ clock: Any = time.monotonic,
78
+ ) -> None:
79
+ self.stream = stream if stream is not None else sys.stderr
80
+ self.mode: RenderMode = mode if mode is not None else select_mode(stream=self.stream)
81
+ self.verbose = verbose
82
+ self.style = Styler(color) if color is not None else styler_for(self.stream)
83
+ self._clock = clock
84
+ self._download_total: int | None = None
85
+ self._layer_bytes: dict[str, int] = {}
86
+ self._line_length = 0
87
+ self._last_redraw: float | None = None
88
+ self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
89
+ self._download_finished = False
90
+
91
+ def __call__(self, event: RuntimeEvent) -> None:
92
+ if self.mode == "quiet":
93
+ return
94
+ if self.verbose:
95
+ self._verbose_line(event)
96
+ return
97
+ handler = getattr(self, "_render_" + event.kind.replace(".", "_"), None)
98
+ if handler is not None:
99
+ handler(event)
100
+
101
+ def note(self, message: str) -> None:
102
+ """Print one renderer-owned status line."""
103
+ if self.mode == "quiet":
104
+ return
105
+ self._print(message)
106
+
107
+ def close(self) -> None:
108
+ """Terminate any in-place progress line before final output."""
109
+ self._end_progress_line()
110
+
111
+ # -- individual event renderings ------------------------------------
112
+
113
+ def _render_package_reference_started(self, event: RuntimeEvent) -> None:
114
+ reference = event.data.get("reference", "")
115
+ self._print(f"Resolving {reference}".rstrip())
116
+
117
+ def _render_discovery_candidate_started(self, event: RuntimeEvent) -> None:
118
+ candidate = event.data.get("candidate", "candidate")
119
+ toolchain = event.data.get("toolchain")
120
+ suffix = f" · {toolchain}" if toolchain else ""
121
+ self._print(f"Trying {candidate}{suffix}")
122
+
123
+ def _render_toolchain_install_started(self, event: RuntimeEvent) -> None:
124
+ toolchain = event.data.get("toolchain", "toolchain")
125
+ self._print(f"Installing Lean toolchain {toolchain} (one-time, can take minutes)")
126
+
127
+ def _render_acquisition_planned(self, event: RuntimeEvent) -> None:
128
+ download = event.data.get("download_bytes")
129
+ cached = event.data.get("cached_bytes")
130
+ if not isinstance(download, int) or download <= 0:
131
+ return
132
+ self._download_total = download
133
+ self._layer_bytes.clear()
134
+ self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
135
+ self._download_finished = False
136
+ self._last_redraw = None
137
+ suffix = (
138
+ f" ({format_byte_size(cached)} already cached)"
139
+ if isinstance(cached, int) and cached > 0
140
+ else ""
141
+ )
142
+ self._print(f"Downloading environment: {format_byte_size(download)}{suffix}")
143
+
144
+ def _render_library_layer_progress(self, event: RuntimeEvent) -> None:
145
+ digest = str(event.data.get("digest", ""))
146
+ if event.current_bytes is None or self._download_total is None:
147
+ return
148
+ self._layer_bytes[digest] = event.current_bytes
149
+ done = min(sum(self._layer_bytes.values()), self._download_total)
150
+ if self.mode == "tty":
151
+ now = self._clock()
152
+ if (
153
+ self._last_redraw is not None
154
+ and done < self._download_total
155
+ and now - self._last_redraw < _TTY_REDRAW_SECONDS
156
+ ):
157
+ return
158
+ self._last_redraw = now
159
+ self._draw_bar(done, self._download_total)
160
+ else:
161
+ percent = done * 100 // self._download_total
162
+ while self._next_checkpoint <= percent:
163
+ self._print(f"Downloaded {self._next_checkpoint}%")
164
+ self._next_checkpoint += _PLAIN_CHECKPOINT_PERCENT
165
+
166
+ def _render_library_layer_download_started(self, event: RuntimeEvent) -> None:
167
+ resumed = event.data.get("resumed_bytes")
168
+ if isinstance(resumed, int) and resumed > 0:
169
+ self._print(f"Resuming interrupted download from {format_byte_size(resumed)}")
170
+
171
+ def _render_library_layer_download_retry(self, event: RuntimeEvent) -> None:
172
+ self._print("Retrying download after integrity verification failed")
173
+
174
+ def _render_library_verified(self, event: RuntimeEvent) -> None:
175
+ if self._download_total is None or self._download_finished:
176
+ return
177
+ self._download_finished = True
178
+ if self.mode == "tty":
179
+ self._draw_bar(self._download_total, self._download_total)
180
+ self._end_progress_line()
181
+ self._print(self.style.green("Downloaded and verified environment"))
182
+
183
+ def _render_source_fetch_started(self, event: RuntimeEvent) -> None:
184
+ package = event.data.get("package", "sources")
185
+ self._print(f"Fetching {package} source")
186
+
187
+ def _render_artifact_hydration_started(self, event: RuntimeEvent) -> None:
188
+ package = event.data.get("package", "artifacts")
189
+ self._print(f"Hydrating build artifacts for {package}")
190
+
191
+ def _render_availability_fallback(self, event: RuntimeEvent) -> None:
192
+ library = event.data.get("library", "environment library")
193
+ reason = event.data.get("reason_code") or event.data.get("reason") or "unavailable"
194
+ self._print(
195
+ self.style.yellow(
196
+ f"WARNING: downloadable environment unavailable from {library} ({reason})"
197
+ )
198
+ )
199
+
200
+ def _render_environment_build_started(self, event: RuntimeEvent) -> None:
201
+ self._print(
202
+ "Building environment from source (large dependencies such as Mathlib can take "
203
+ "30+ minutes; pass --no-source-build to fail fast instead)"
204
+ )
205
+
206
+ def _render_capability_required(self, event: RuntimeEvent) -> None:
207
+ self._print(event.message)
208
+
209
+ # -- low-level output -----------------------------------------------
210
+
211
+ def _verbose_line(self, event: RuntimeEvent) -> None:
212
+ parts = [f"{event.kind}: {event.message}"]
213
+ if event.current_bytes is not None and event.total_bytes is not None:
214
+ parts.append(
215
+ f"[{format_byte_size(event.current_bytes)}/{format_byte_size(event.total_bytes)}]"
216
+ )
217
+ if event.data:
218
+ details = " ".join(f"{key}={value}" for key, value in sorted(event.data.items()))
219
+ parts.append(f"({details})")
220
+ self._print(" ".join(parts))
221
+
222
+ def _draw_bar(self, done: int, total: int) -> None:
223
+ filled = _BAR_WIDTH * done // total if total else _BAR_WIDTH
224
+ bar = "█" * filled + "░" * (_BAR_WIDTH - filled)
225
+ percent = done * 100 // total if total else 100
226
+ sizes = f"{format_byte_size(done)}/{format_byte_size(total)}"
227
+ plain = f"Downloading [{bar}] {percent}% · {sizes}"
228
+ line = (
229
+ f"Downloading [{self.style.cyan(bar)}] "
230
+ f"{self.style.bold(f'{percent}%')} · {self.style.dim(sizes)}"
231
+ )
232
+ padding = " " * max(0, self._line_length - len(plain))
233
+ self.stream.write("\r" + line + padding)
234
+ self.stream.flush()
235
+ self._line_length = len(plain)
236
+
237
+ def _end_progress_line(self) -> None:
238
+ if self._line_length:
239
+ self.stream.write("\n")
240
+ self.stream.flush()
241
+ self._line_length = 0
242
+
243
+ def _print(self, message: str) -> None:
244
+ self._end_progress_line()
245
+ self.stream.write(message + "\n")
246
+ self.stream.flush()
@@ -78,6 +78,14 @@ class LeanRuntimeProbe:
78
78
  cancel: threading.Event,
79
79
  ) -> AcquiredCandidate:
80
80
  event_offset = len(self.events) if self.events is not None else 0
81
+ emitter = getattr(self.runtime, "events", None)
82
+ if emitter is not None:
83
+ emitter.emit(
84
+ "discovery.candidate_started",
85
+ f"Trying {candidate.entry.id}",
86
+ candidate=candidate.entry.id,
87
+ toolchain=candidate.entry.toolchain,
88
+ )
81
89
  try:
82
90
  environment = self.runtime.open_exact(
83
91
  candidate.entry.lock,
@@ -64,6 +64,15 @@ class DownloadUnavailable(EnvironmentError):
64
64
  """A prebuilt cache had no usable artifact or could not be reached."""
65
65
 
66
66
 
67
+ class DownloadLimitExceeded(EnvironmentError):
68
+ """An acquisition needs more bytes than the configured download limit.
69
+
70
+ Deliberately not a :class:`DownloadUnavailable`: exceeding a policy limit
71
+ must fail the operation instead of silently falling back to another
72
+ library or a source build.
73
+ """
74
+
75
+
67
76
  class MaterializationError(EnvironmentError):
68
77
  """Locked sources or build artifacts could not be materialized."""
69
78
 
@@ -16,9 +16,16 @@ class RuntimeEvent:
16
16
  message: str
17
17
  data: dict[str, Any] = field(default_factory=dict)
18
18
  timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
19
+ phase: str | None = None
20
+ current_bytes: int | None = None
21
+ total_bytes: int | None = None
19
22
 
20
23
  def to_dict(self) -> dict[str, Any]:
21
- return asdict(self)
24
+ value = asdict(self)
25
+ for key in ("phase", "current_bytes", "total_bytes"):
26
+ if value[key] is None:
27
+ del value[key]
28
+ return value
22
29
 
23
30
 
24
31
  EventCallback = Callable[[RuntimeEvent], None]
@@ -30,6 +37,24 @@ class EventEmitter:
30
37
  def __init__(self, callback: EventCallback | None = None) -> None:
31
38
  self.callback = callback
32
39
 
33
- def emit(self, kind: str, message: str, **data: Any) -> None:
40
+ def emit(
41
+ self,
42
+ kind: str,
43
+ message: str,
44
+ *,
45
+ phase: str | None = None,
46
+ current_bytes: int | None = None,
47
+ total_bytes: int | None = None,
48
+ **data: Any,
49
+ ) -> None:
34
50
  if self.callback is not None:
35
- self.callback(RuntimeEvent(kind=kind, message=message, data=data))
51
+ self.callback(
52
+ RuntimeEvent(
53
+ kind=kind,
54
+ message=message,
55
+ data=data,
56
+ phase=phase,
57
+ current_bytes=current_bytes,
58
+ total_bytes=total_bytes,
59
+ )
60
+ )