phasesweep 0.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. phasesweep-0.3.6/.github/workflows/ci.yml +41 -0
  2. phasesweep-0.3.6/.github/workflows/publish.yml +155 -0
  3. phasesweep-0.3.6/.gitignore +180 -0
  4. phasesweep-0.3.6/.gitmodules +3 -0
  5. phasesweep-0.3.6/.pre-commit-config.yaml +67 -0
  6. phasesweep-0.3.6/LICENSE +21 -0
  7. phasesweep-0.3.6/PKG-INFO +206 -0
  8. phasesweep-0.3.6/README.md +161 -0
  9. phasesweep-0.3.6/docs/config.md +381 -0
  10. phasesweep-0.3.6/docs/config_reference.yaml +231 -0
  11. phasesweep-0.3.6/docs/development.md +288 -0
  12. phasesweep-0.3.6/docs/images/diagramA_dag.png +0 -0
  13. phasesweep-0.3.6/docs/images/diagramB_statemachine.png +0 -0
  14. phasesweep-0.3.6/docs/images/diagramE_guardlayer.png +0 -0
  15. phasesweep-0.3.6/docs/invariants.md +528 -0
  16. phasesweep-0.3.6/docs/mcp.md +232 -0
  17. phasesweep-0.3.6/docs/mcp_setup.md +101 -0
  18. phasesweep-0.3.6/docs/runtime.md +298 -0
  19. phasesweep-0.3.6/examples/catalog.yaml +17 -0
  20. phasesweep-0.3.6/examples/experiment.yaml +86 -0
  21. phasesweep-0.3.6/examples/mcp_experiment.yaml +70 -0
  22. phasesweep-0.3.6/examples/tiny_decoder_enwik8/README.md +112 -0
  23. phasesweep-0.3.6/examples/tiny_decoder_enwik8/catalog.yaml +15 -0
  24. phasesweep-0.3.6/examples/tiny_decoder_enwik8/experiment.yaml +79 -0
  25. phasesweep-0.3.6/examples/tiny_decoder_enwik8/gpu_smoke.yaml +64 -0
  26. phasesweep-0.3.6/examples/tiny_decoder_enwik8/mcp_experiment.yaml +79 -0
  27. phasesweep-0.3.6/examples/tiny_decoder_enwik8/run_trial.py +235 -0
  28. phasesweep-0.3.6/pyproject.toml +136 -0
  29. phasesweep-0.3.6/scripts/check_module_size.sh +34 -0
  30. phasesweep-0.3.6/setup.cfg +4 -0
  31. phasesweep-0.3.6/src/phasesweep/__init__.py +31 -0
  32. phasesweep-0.3.6/src/phasesweep/__main__.py +5 -0
  33. phasesweep-0.3.6/src/phasesweep/_metadata.py +11 -0
  34. phasesweep-0.3.6/src/phasesweep/cli.py +832 -0
  35. phasesweep-0.3.6/src/phasesweep/config/__init__.py +64 -0
  36. phasesweep-0.3.6/src/phasesweep/config/common.py +207 -0
  37. phasesweep-0.3.6/src/phasesweep/config/io.py +173 -0
  38. phasesweep-0.3.6/src/phasesweep/config/models.py +1219 -0
  39. phasesweep-0.3.6/src/phasesweep/config/search.py +521 -0
  40. phasesweep-0.3.6/src/phasesweep/engine/__init__.py +72 -0
  41. phasesweep-0.3.6/src/phasesweep/engine/artifact_roots.py +416 -0
  42. phasesweep-0.3.6/src/phasesweep/engine/artifacts.py +395 -0
  43. phasesweep-0.3.6/src/phasesweep/engine/attempts.py +1235 -0
  44. phasesweep-0.3.6/src/phasesweep/engine/cleanup.py +245 -0
  45. phasesweep-0.3.6/src/phasesweep/engine/confound.py +512 -0
  46. phasesweep-0.3.6/src/phasesweep/engine/errors.py +179 -0
  47. phasesweep-0.3.6/src/phasesweep/engine/evidence.py +716 -0
  48. phasesweep-0.3.6/src/phasesweep/engine/fingerprints.py +268 -0
  49. phasesweep-0.3.6/src/phasesweep/engine/generation.py +512 -0
  50. phasesweep-0.3.6/src/phasesweep/engine/guards.py +223 -0
  51. phasesweep-0.3.6/src/phasesweep/engine/ledger.py +1389 -0
  52. phasesweep-0.3.6/src/phasesweep/engine/locking.py +175 -0
  53. phasesweep-0.3.6/src/phasesweep/engine/optuna.py +309 -0
  54. phasesweep-0.3.6/src/phasesweep/engine/paths.py +208 -0
  55. phasesweep-0.3.6/src/phasesweep/engine/phase.py +1685 -0
  56. phasesweep-0.3.6/src/phasesweep/engine/provenance.py +127 -0
  57. phasesweep-0.3.6/src/phasesweep/engine/publication.py +200 -0
  58. phasesweep-0.3.6/src/phasesweep/engine/publication_validation.py +880 -0
  59. phasesweep-0.3.6/src/phasesweep/engine/read.py +818 -0
  60. phasesweep-0.3.6/src/phasesweep/engine/resume.py +229 -0
  61. phasesweep-0.3.6/src/phasesweep/engine/run.py +870 -0
  62. phasesweep-0.3.6/src/phasesweep/engine/selection.py +241 -0
  63. phasesweep-0.3.6/src/phasesweep/engine/state.py +238 -0
  64. phasesweep-0.3.6/src/phasesweep/engine/study_policy.py +650 -0
  65. phasesweep-0.3.6/src/phasesweep/engine/trial.py +947 -0
  66. phasesweep-0.3.6/src/phasesweep/errors.py +134 -0
  67. phasesweep-0.3.6/src/phasesweep/evidence/__init__.py +19 -0
  68. phasesweep-0.3.6/src/phasesweep/evidence/evaluation.py +936 -0
  69. phasesweep-0.3.6/src/phasesweep/evidence/models.py +626 -0
  70. phasesweep-0.3.6/src/phasesweep/evidence/wandb.py +736 -0
  71. phasesweep-0.3.6/src/phasesweep/examples/__init__.py +1 -0
  72. phasesweep-0.3.6/src/phasesweep/examples/fake_train.py +168 -0
  73. phasesweep-0.3.6/src/phasesweep/mcp/__init__.py +30 -0
  74. phasesweep-0.3.6/src/phasesweep/mcp/agent_prompt.md +16 -0
  75. phasesweep-0.3.6/src/phasesweep/mcp/audit.py +114 -0
  76. phasesweep-0.3.6/src/phasesweep/mcp/errors.py +277 -0
  77. phasesweep-0.3.6/src/phasesweep/mcp/recovery.py +973 -0
  78. phasesweep-0.3.6/src/phasesweep/mcp/redaction.py +269 -0
  79. phasesweep-0.3.6/src/phasesweep/mcp/registry.py +620 -0
  80. phasesweep-0.3.6/src/phasesweep/mcp/run_control.py +942 -0
  81. phasesweep-0.3.6/src/phasesweep/mcp/runner.py +1044 -0
  82. phasesweep-0.3.6/src/phasesweep/mcp/runs.py +1647 -0
  83. phasesweep-0.3.6/src/phasesweep/mcp/scaffold.py +129 -0
  84. phasesweep-0.3.6/src/phasesweep/mcp/server.py +943 -0
  85. phasesweep-0.3.6/src/phasesweep/mcp/snapshots.py +660 -0
  86. phasesweep-0.3.6/src/phasesweep/mcp/tool_names.py +16 -0
  87. phasesweep-0.3.6/src/phasesweep/mcp/tools.py +848 -0
  88. phasesweep-0.3.6/src/phasesweep/py.typed +1 -0
  89. phasesweep-0.3.6/src/phasesweep/reporting.py +195 -0
  90. phasesweep-0.3.6/src/phasesweep/runtime/__init__.py +1 -0
  91. phasesweep-0.3.6/src/phasesweep/runtime/commands.py +422 -0
  92. phasesweep-0.3.6/src/phasesweep/runtime/files.py +1343 -0
  93. phasesweep-0.3.6/src/phasesweep/runtime/gpu.py +976 -0
  94. phasesweep-0.3.6/src/phasesweep/runtime/json.py +66 -0
  95. phasesweep-0.3.6/src/phasesweep/runtime/process.py +1189 -0
  96. phasesweep-0.3.6/src/phasesweep/runtime/reaper.py +782 -0
  97. phasesweep-0.3.6/src/phasesweep/runtime/shutdown.py +662 -0
  98. phasesweep-0.3.6/src/phasesweep/runtime/supervisor.py +349 -0
  99. phasesweep-0.3.6/src/phasesweep/runtime/time.py +36 -0
  100. phasesweep-0.3.6/src/phasesweep/templates/starter_experiment.yaml +57 -0
  101. phasesweep-0.3.6/src/phasesweep.egg-info/PKG-INFO +206 -0
  102. phasesweep-0.3.6/src/phasesweep.egg-info/SOURCES.txt +194 -0
  103. phasesweep-0.3.6/src/phasesweep.egg-info/dependency_links.txt +1 -0
  104. phasesweep-0.3.6/src/phasesweep.egg-info/entry_points.txt +3 -0
  105. phasesweep-0.3.6/src/phasesweep.egg-info/requires.txt +22 -0
  106. phasesweep-0.3.6/src/phasesweep.egg-info/scm_file_list.json +190 -0
  107. phasesweep-0.3.6/src/phasesweep.egg-info/scm_version.json +8 -0
  108. phasesweep-0.3.6/src/phasesweep.egg-info/top_level.txt +1 -0
  109. phasesweep-0.3.6/tests/__init__.py +0 -0
  110. phasesweep-0.3.6/tests/conftest.py +785 -0
  111. phasesweep-0.3.6/tests/fixtures/__init__.py +1 -0
  112. phasesweep-0.3.6/tests/fixtures/ledgers/README.md +88 -0
  113. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/.gitignore +3 -0
  114. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/artifact_root_binding.json +1 -0
  115. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generation.yaml +6 -0
  116. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generations/cd4512ad12d842a1ab02d436d9e82348/config.snapshot.yaml +55 -0
  117. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generations/cd4512ad12d842a1ab02d436d9e82348/generation.yaml +6 -0
  118. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generations/cd4512ad12d842a1ab02d436d9e82348/phases/p/winner.yaml +69 -0
  119. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generations/cd4512ad12d842a1ab02d436d9e82348/reproducibility.json +28 -0
  120. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/generations/cd4512ad12d842a1ab02d436d9e82348/summary.yaml +64 -0
  121. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/last_successful_generation.yaml +5 -0
  122. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/attempt_lifecycle.json +1 -0
  123. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/command.txt +1 -0
  124. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/overrides_resolved.json +3 -0
  125. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/process_identity.json +1 -0
  126. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/stderr.log +0 -0
  127. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00000__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_cc31794be3a342f484b06f055ff72700/stdout.log +1 -0
  128. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/attempt_lifecycle.json +1 -0
  129. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/command.txt +1 -0
  130. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/overrides_resolved.json +3 -0
  131. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/process_identity.json +1 -0
  132. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/stderr.log +0 -0
  133. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trial_00001__generation_cd4512ad12d842a1ab02d436d9e82348__attempt_2853324d9a4c4d4388b55d279351b9ab/stdout.log +1 -0
  134. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/trials.csv +3 -0
  135. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/p/winner.yaml +69 -0
  136. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/run.log +9 -0
  137. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/artifact_root/t/summary.yaml +64 -0
  138. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/ledger/study.journal +38 -0
  139. phasesweep-0.3.6/tests/fixtures/ledgers/current-journal/manifest.json +29 -0
  140. phasesweep-0.3.6/tests/fixtures/make_ledger_fixtures.py +577 -0
  141. phasesweep-0.3.6/tests/ledger_fixtures.py +463 -0
  142. phasesweep-0.3.6/tests/mcp_helpers.py +496 -0
  143. phasesweep-0.3.6/tests/recovery_helpers.py +240 -0
  144. phasesweep-0.3.6/tests/test_abort_recovery.py +318 -0
  145. phasesweep-0.3.6/tests/test_attempt_resolution.py +86 -0
  146. phasesweep-0.3.6/tests/test_cli.py +1039 -0
  147. phasesweep-0.3.6/tests/test_config.py +865 -0
  148. phasesweep-0.3.6/tests/test_configuration_revalidation.py +199 -0
  149. phasesweep-0.3.6/tests/test_confound.py +721 -0
  150. phasesweep-0.3.6/tests/test_e2e.py +173 -0
  151. phasesweep-0.3.6/tests/test_engine_read.py +946 -0
  152. phasesweep-0.3.6/tests/test_engine_state.py +20 -0
  153. phasesweep-0.3.6/tests/test_engine_status_shape.py +177 -0
  154. phasesweep-0.3.6/tests/test_error_routing.py +1771 -0
  155. phasesweep-0.3.6/tests/test_extractors.py +1291 -0
  156. phasesweep-0.3.6/tests/test_filesystem_layout.py +290 -0
  157. phasesweep-0.3.6/tests/test_fingerprint.py +2073 -0
  158. phasesweep-0.3.6/tests/test_format_cutover.py +1077 -0
  159. phasesweep-0.3.6/tests/test_gpu_pool.py +1136 -0
  160. phasesweep-0.3.6/tests/test_init.py +241 -0
  161. phasesweep-0.3.6/tests/test_ledger_contract.py +703 -0
  162. phasesweep-0.3.6/tests/test_ledger_read_paths.py +360 -0
  163. phasesweep-0.3.6/tests/test_locking.py +864 -0
  164. phasesweep-0.3.6/tests/test_mcp_cancel.py +459 -0
  165. phasesweep-0.3.6/tests/test_mcp_check.py +339 -0
  166. phasesweep-0.3.6/tests/test_mcp_e2e.py +667 -0
  167. phasesweep-0.3.6/tests/test_mcp_launch.py +1732 -0
  168. phasesweep-0.3.6/tests/test_mcp_read_tools.py +1065 -0
  169. phasesweep-0.3.6/tests/test_mcp_recovery.py +1824 -0
  170. phasesweep-0.3.6/tests/test_mcp_redaction.py +328 -0
  171. phasesweep-0.3.6/tests/test_mcp_registry.py +575 -0
  172. phasesweep-0.3.6/tests/test_mcp_runner.py +2404 -0
  173. phasesweep-0.3.6/tests/test_mcp_runs.py +1746 -0
  174. phasesweep-0.3.6/tests/test_mcp_scaffold.py +292 -0
  175. phasesweep-0.3.6/tests/test_mcp_status_timing.py +560 -0
  176. phasesweep-0.3.6/tests/test_overrides.py +781 -0
  177. phasesweep-0.3.6/tests/test_param_validation.py +929 -0
  178. phasesweep-0.3.6/tests/test_process_supervision.py +2206 -0
  179. phasesweep-0.3.6/tests/test_protocol.py +213 -0
  180. phasesweep-0.3.6/tests/test_public_metadata.py +217 -0
  181. phasesweep-0.3.6/tests/test_publication_transaction.py +1644 -0
  182. phasesweep-0.3.6/tests/test_reporting.py +288 -0
  183. phasesweep-0.3.6/tests/test_runner_signal_guard.py +86 -0
  184. phasesweep-0.3.6/tests/test_runtime_behavior.py +1791 -0
  185. phasesweep-0.3.6/tests/test_selector.py +277 -0
  186. phasesweep-0.3.6/tests/test_shutdown.py +599 -0
  187. phasesweep-0.3.6/tests/test_stale_reaper.py +1985 -0
  188. phasesweep-0.3.6/tests/test_storage_urls.py +255 -0
  189. phasesweep-0.3.6/tests/test_tier_guard.py +317 -0
  190. phasesweep-0.3.6/tests/test_timeouts.py +917 -0
  191. phasesweep-0.3.6/tests/test_tiny_decoder_example.py +196 -0
  192. phasesweep-0.3.6/tests/test_trial_evidence.py +1120 -0
  193. phasesweep-0.3.6/tests/test_trial_launch.py +675 -0
  194. phasesweep-0.3.6/tests/test_wandb_live.py +177 -0
  195. phasesweep-0.3.6/tests/tiers.py +527 -0
  196. phasesweep-0.3.6/tests/wandb_live_trainer.py +64 -0
@@ -0,0 +1,41 @@
1
+ name: Checks
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ linux:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.13"
17
+ - name: Install static-check dependencies
18
+ run: >-
19
+ python -m pip install
20
+ 'ruff>=0.5' 'mypy>=1.10' 'types-PyYAML>=6.0'
21
+ 'optuna==4.9.0' 'cmaes>=0.10'
22
+ 'pydantic>=2.0' 'pyyaml>=6.0' 'click>=8.1' 'mcp>=1.27,<1.28'
23
+ - run: ruff check .
24
+ - run: ruff format --check .
25
+ - run: mypy src
26
+ # Fast in-process contracts, run from src/ without building the package.
27
+ # The maintainer approved this step for one Python version within about
28
+ # 20 seconds of CI time; anything slower stays local with the full suite.
29
+ # Collecting the whole suite runs the integration-tier guard over every
30
+ # test module without executing one; it imports each module, so a test
31
+ # that needs Hydra or W&B must import it inside the test. The size check
32
+ # covers every tracked src and test module, not only the ones a commit
33
+ # touched.
34
+ - name: Contract tests
35
+ run: |
36
+ bash scripts/check_module_size.sh
37
+ python -m pip install 'pytest>=7'
38
+ PYTHONPATH=src python -m pytest --collect-only -q -p no:cacheprovider
39
+ PYTHONPATH=src python -m pytest -p no:cacheprovider \
40
+ tests/test_ledger_contract.py tests/test_error_routing.py \
41
+ tests/test_tier_guard.py tests/test_ledger_read_paths.py
@@ -0,0 +1,155 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ # Scope concurrency by release tag so distinct releases cannot replace an older pending run.
11
+ concurrency:
12
+ group: pypi-publish-${{ github.event.release.tag_name }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build-distributions:
17
+ if: github.event.release.prerelease == false
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 15
20
+ steps:
21
+ - name: Check out the release commit
22
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
+ with:
24
+ ref: ${{ github.sha }}
25
+ fetch-depth: 0
26
+ persist-credentials: false
27
+ - name: Set up Python
28
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
29
+ with:
30
+ python-version: "3.13"
31
+ - name: Install packaging tools
32
+ run: python -m pip install build packaging twine
33
+ - name: Validate the release tag and commit
34
+ id: release
35
+ env:
36
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
37
+ run: |
38
+ if ! git merge-base --is-ancestor HEAD origin/main; then
39
+ echo "Release tag ${RELEASE_TAG} is not contained in main." >&2
40
+ exit 1
41
+ fi
42
+ python - <<'PY'
43
+ import os
44
+ from pathlib import Path
45
+
46
+ from packaging.version import InvalidVersion, Version
47
+
48
+ tag = os.environ["RELEASE_TAG"]
49
+ if not tag.startswith("v"):
50
+ raise SystemExit(f"release tag must start with 'v': {tag!r}")
51
+ try:
52
+ version = Version(tag[1:])
53
+ except InvalidVersion as error:
54
+ raise SystemExit(f"release tag is not a PEP 440 version: {tag!r}") from error
55
+ # Post releases and epochs remain valid stable public PEP 440 versions.
56
+ if version.is_prerelease or version.is_devrelease or version.local is not None:
57
+ raise SystemExit(f"release tag must be a stable public version: {tag!r}")
58
+ with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
59
+ print(f"version={version}", file=output)
60
+ PY
61
+ - name: Build distributions
62
+ env:
63
+ # An annotated RC tag on the same commit must not override the release tag.
64
+ SETUPTOOLS_SCM_OVERRIDES_FOR_PHASESWEEP: >-
65
+ {scm.git.describe_command = ["git", "describe", "--dirty", "--tags",
66
+ "--long", "--abbrev=40", "--exact-match", "--match",
67
+ "${{ github.event.release.tag_name }}"]}
68
+ run: python -m build
69
+ - name: Check distribution metadata
70
+ run: python -m twine check --strict dist/*
71
+ - name: Verify distribution versions
72
+ env:
73
+ EXPECTED_VERSION: ${{ steps.release.outputs.version }}
74
+ run: |
75
+ python - <<'PY'
76
+ import os
77
+ import tarfile
78
+ from email.parser import BytesParser
79
+ from pathlib import Path
80
+ from zipfile import ZipFile
81
+
82
+ from packaging.utils import canonicalize_name
83
+ from packaging.version import Version
84
+
85
+ expected_version = Version(os.environ["EXPECTED_VERSION"])
86
+ wheels = list(Path("dist").glob("*.whl"))
87
+ sdists = list(Path("dist").glob("*.tar.gz"))
88
+ if len(wheels) != 1 or len(sdists) != 1:
89
+ raise SystemExit(f"expected one wheel and one sdist, found {wheels!r} and {sdists!r}")
90
+ with ZipFile(wheels[0]) as archive:
91
+ metadata_paths = [
92
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
93
+ ]
94
+ if len(metadata_paths) != 1:
95
+ raise SystemExit(
96
+ f"expected one .dist-info/METADATA in {wheels[0]}, found {metadata_paths!r}"
97
+ )
98
+ metadata_path = metadata_paths[0]
99
+ wheel_metadata = BytesParser().parsebytes(archive.read(metadata_path))
100
+ with tarfile.open(sdists[0]) as archive:
101
+ metadata_members = [
102
+ member
103
+ for member in archive.getmembers()
104
+ if member.name.endswith("/PKG-INFO") and len(Path(member.name).parts) == 2
105
+ ]
106
+ if len(metadata_members) != 1:
107
+ names = [member.name for member in metadata_members]
108
+ raise SystemExit(
109
+ f"expected one top-level PKG-INFO in {sdists[0]}, found {names!r}"
110
+ )
111
+ metadata_member = metadata_members[0]
112
+ metadata_file = archive.extractfile(metadata_member)
113
+ if metadata_file is None:
114
+ raise SystemExit(f"could not read {metadata_member.name} from {sdists[0]}")
115
+ sdist_metadata = BytesParser().parse(metadata_file)
116
+ for artifact, metadata in (
117
+ (wheels[0], wheel_metadata),
118
+ (sdists[0], sdist_metadata),
119
+ ):
120
+ if canonicalize_name(metadata["Name"]) != "phasesweep":
121
+ raise SystemExit(f"unexpected project name in {artifact}: {metadata['Name']!r}")
122
+ actual_version = Version(metadata["Version"])
123
+ if actual_version != expected_version:
124
+ raise SystemExit(
125
+ f"release tag expects {expected_version}, but {artifact} contains {actual_version}"
126
+ )
127
+ PY
128
+ - name: Store distributions
129
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
130
+ with:
131
+ name: release-distributions
132
+ path: dist/
133
+ if-no-files-found: error
134
+ retention-days: 7
135
+
136
+ publish-to-pypi:
137
+ if: github.event.release.prerelease == false
138
+ needs: build-distributions
139
+ runs-on: ubuntu-latest
140
+ timeout-minutes: 10
141
+ environment:
142
+ name: pypi
143
+ url: https://pypi.org/p/phasesweep
144
+ permissions:
145
+ id-token: write
146
+ steps:
147
+ - name: Retrieve distributions
148
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
149
+ with:
150
+ name: release-distributions
151
+ path: dist/
152
+ - name: Publish distributions to PyPI
153
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
154
+ with:
155
+ attestations: true
@@ -0,0 +1,180 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+ _version.py
29
+
30
+ # phasesweep local outputs
31
+ runs/
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py.cover
54
+ *.lcov
55
+ .hypothesis/
56
+ .pytest_cache/
57
+ cover/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ .pybuilder/
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # UV
91
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
92
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
93
+ # commonly ignored for libraries.
94
+ # uv.lock
95
+
96
+
97
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule*
102
+ celerybeat.pid
103
+
104
+ # Redis
105
+ *.rdb
106
+ *.aof
107
+ *.pid
108
+
109
+ # RabbitMQ
110
+ mnesia/
111
+ rabbitmq/
112
+ rabbitmq-data/
113
+
114
+ # ActiveMQ
115
+ activemq-data/
116
+
117
+ # SageMath parsed files
118
+ *.sage.py
119
+
120
+ # Environments
121
+ .env
122
+ .envrc
123
+ .venv
124
+ env/
125
+ venv/
126
+ ENV/
127
+ env.bak/
128
+ venv.bak/
129
+
130
+ # Spyder project settings
131
+ .spyderproject
132
+ .spyproject
133
+
134
+ # Rope project settings
135
+ .ropeproject
136
+
137
+ # mkdocs documentation
138
+ /site
139
+
140
+ # mypy
141
+ .mypy_cache/
142
+ .dmypy.json
143
+ dmypy.json
144
+
145
+ # Pyre type checker
146
+ .pyre/
147
+
148
+ # pytype static type analyzer
149
+ .pytype/
150
+
151
+ # Cython debug symbols
152
+ cython_debug/
153
+
154
+ # Visual Studio Code
155
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
156
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
157
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
158
+ # you could uncomment the following to ignore the entire vscode folder
159
+ # .vscode/
160
+ # Temporary file for partial code execution
161
+ tempCodeRunnerFile.py
162
+
163
+ # Ruff stuff:
164
+ .ruff_cache/
165
+
166
+ # PyPI configuration file
167
+ .pypirc
168
+
169
+ # Marimo
170
+ marimo/_static/
171
+ marimo/_lsp/
172
+ __marimo__/
173
+
174
+ # Streamlit
175
+ .streamlit/secrets.toml
176
+
177
+ # Golden ledger fixtures are committed on purpose: real PhaseSweep output whose
178
+ # trial logs (*.log) and artifact trees would otherwise be ignored above.
179
+ # See tests/fixtures/ledgers/README.md.
180
+ !tests/fixtures/ledgers/**
@@ -0,0 +1,3 @@
1
+ [submodule "examples/tiny_decoder_enwik8/upstream"]
2
+ path = examples/tiny_decoder_enwik8/upstream
3
+ url = https://github.com/pszemraj/decoder-pytorch-template.git
@@ -0,0 +1,67 @@
1
+ # Every hook is `repo: local` with `language: system`: it runs the tool already
2
+ # installed in the active development environment (`pip install -e ".[dev]"`),
3
+ # so pre-commit builds no per-hook environments, needs no network, and uses the
4
+ # same tool versions as manual runs. Activate that environment before
5
+ # committing or pushing.
6
+ minimum_pre_commit_version: "3.2.0"
7
+ default_install_hook_types: [pre-commit, pre-push]
8
+ default_stages: [pre-commit]
9
+
10
+ repos:
11
+ - repo: local
12
+ hooks:
13
+ - id: ruff
14
+ name: ruff check
15
+ entry: ruff check --force-exclude --fix
16
+ language: system
17
+ types_or: [python, pyi]
18
+ require_serial: true
19
+ - id: ruff-format
20
+ name: ruff format
21
+ entry: ruff format --force-exclude
22
+ language: system
23
+ types_or: [python, pyi]
24
+ require_serial: true
25
+ - id: pathlint
26
+ name: pathlint
27
+ entry: pathlint
28
+ language: system
29
+ files: ^(src|tests)/.*\.py$
30
+ # doc_check.py is a maintainer script outside the repository; without it
31
+ # the hook reports the skip and passes. Its report prints only on failure.
32
+ - id: doc-check
33
+ name: doc-check (strict)
34
+ entry: >-
35
+ bash -c 'script="$HOME/scripts/py/doc_check.py";
36
+ [ -f "$script" ] || { echo "doc_check.py not found; skipped"; exit 0; };
37
+ out=$(python "$script" --strict "$@") || { printf "%s\n" "$out"; exit 1; }' --
38
+ language: system
39
+ files: ^src/.*\.py$
40
+ verbose: true
41
+ - id: module-size
42
+ name: module size
43
+ entry: bash scripts/check_module_size.sh
44
+ language: system
45
+ files: ^(src|tests)/.*\.py$
46
+ # Runs on every commit: the contracts span all of src/ and the pytest
47
+ # configuration.
48
+ - id: contract-tests
49
+ name: contract tests
50
+ entry: pytest tests/test_ledger_contract.py tests/test_error_routing.py tests/test_tier_guard.py -q -p no:cacheprovider
51
+ language: system
52
+ pass_filenames: false
53
+ always_run: true
54
+ # Collecting the whole suite runs the integration-tier guard over every
55
+ # test module without executing any test.
56
+ - id: tier-guard
57
+ name: tier guard (whole-suite collection)
58
+ entry: pytest --collect-only -q -p no:cacheprovider
59
+ language: system
60
+ pass_filenames: false
61
+ always_run: true
62
+ - id: mypy
63
+ name: mypy
64
+ entry: mypy src
65
+ language: system
66
+ pass_filenames: false
67
+ stages: [pre-push]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Szemraj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: phasesweep
3
+ Version: 0.3.6
4
+ Summary: YAML-driven phase-chained hyperparameter sweeps with Optuna: each phase's winner is set for all following sweeps.
5
+ Author: Peter Szemraj
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pszemraj/phasesweep
8
+ Project-URL: Documentation, https://github.com/pszemraj/phasesweep/tree/main/docs
9
+ Project-URL: Repository, https://github.com/pszemraj/phasesweep.git
10
+ Project-URL: Issues, https://github.com/pszemraj/phasesweep/issues
11
+ Keywords: experiments,hyperparameter-optimization,machine-learning,optuna,sweeps
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: optuna<4.10,>=4.0
26
+ Requires-Dist: cmaes>=0.10
27
+ Requires-Dist: pydantic>=2.0
28
+ Requires-Dist: pyyaml>=6.0
29
+ Requires-Dist: click>=8.1
30
+ Provides-Extra: wandb
31
+ Requires-Dist: wandb<0.29,>=0.28; extra == "wandb"
32
+ Provides-Extra: mcp
33
+ Requires-Dist: mcp<1.28,>=1.27; extra == "mcp"
34
+ Provides-Extra: dev
35
+ Requires-Dist: hydra-core<1.4,>=1.3; extra == "dev"
36
+ Requires-Dist: pytest>=7; extra == "dev"
37
+ Requires-Dist: setuptools-scm[simple]>=9.2; extra == "dev"
38
+ Requires-Dist: ruff>=0.5; extra == "dev"
39
+ Requires-Dist: mypy>=1.10; extra == "dev"
40
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
41
+ Requires-Dist: mcp<1.28,>=1.27; extra == "dev"
42
+ Requires-Dist: pre-commit>=3.2; extra == "dev"
43
+ Requires-Dist: pathlint>=0.2; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # PhaseSweep
47
+
48
+ PhaseSweep runs phase-chained hyperparameter sweeps from one ordinary YAML file. That file contains your trainer configuration, the metric, and the search plan. PhaseSweep materializes the configured trainer input for every trial, decides what to try next, persists each phase winner, and carries selected values forward as fixed inputs to later phases.
49
+
50
+ This is useful when a full joint sweep is too expensive or hard to interpret. For example, choose architecture depth, then tune learning rate, then regularization. The [configuration guide](https://github.com/pszemraj/phasesweep/blob/main/docs/config.md#phase-composition) explains the inheritance model and its tradeoffs.
51
+
52
+ ![PhaseSweep phase DAG](https://raw.githubusercontent.com/pszemraj/phasesweep/main/docs/images/diagramA_dag.png)
53
+
54
+ ## How it works
55
+
56
+ The starter generated by `phasesweep init` searches depth before learning rate (abridged; the full file also sets storage, the working directory, and the metric extractor):
57
+
58
+ ```yaml
59
+ experiment: phasesweep_starter
60
+
61
+ # The trainer receives one complete generated YAML per trial.
62
+ trial_command: "python -m phasesweep.examples.fake_train --config {config_path}"
63
+
64
+ trainer_config:
65
+ model:
66
+ n_layers: 8
67
+ dropout: 0.1
68
+ optimizer:
69
+ lr: 0.0003
70
+ weight_decay: 0.05
71
+
72
+ metric:
73
+ name: eval_loss
74
+ goal: minimize
75
+
76
+ phases:
77
+ - name: depth
78
+ n_trials: 2
79
+ sampler: { type: grid }
80
+ search_space:
81
+ model.n_layers: { type: categorical, choices: [6, 8] }
82
+
83
+ - name: learning_rate
84
+ inherits: [depth] # the winning model.n_layers is fixed here
85
+ n_trials: 2
86
+ sampler: { type: grid }
87
+ search_space:
88
+ optimizer.lr: { type: categorical, choices: [0.0001, 0.0003] }
89
+ ```
90
+
91
+ The packaged fake trainer uses the optional `report_objective(...)` envelope
92
+ helper. Your own trainer can report through W&B, ordinary JSON, or logs without
93
+ importing PhaseSweep.
94
+
95
+ ## Install and try it
96
+
97
+ Requirements: Python 3.11+ and a POSIX host for real runs. GPUs are optional.
98
+
99
+ ```bash
100
+ python -m pip install phasesweep
101
+ phasesweep --version
102
+
103
+ mkdir phasesweep-demo && cd phasesweep-demo
104
+ phasesweep init # writes the starter experiment.yaml
105
+ phasesweep validate experiment.yaml # checks it without launching anything
106
+ phasesweep run experiment.yaml # four tiny trials, a few seconds
107
+ ```
108
+
109
+ `phasesweep init` never overwrites an existing file; use `-o PATH` for another
110
+ destination.
111
+
112
+ The run log shows the phase chaining directly (abridged):
113
+
114
+ ```text
115
+ [depth/trial_0] python -m phasesweep.examples.fake_train --config .../trainer_config.yaml
116
+ [depth/trial_1] python -m phasesweep.examples.fake_train --config .../trainer_config.yaml
117
+ phase=depth WINNER trial=0 metric=0.3 params={'model.n_layers': 8}
118
+ [learning_rate/trial_0] python -m phasesweep.examples.fake_train --config .../trainer_config.yaml
119
+ [learning_rate/trial_1] python -m phasesweep.examples.fake_train --config .../trainer_config.yaml
120
+ phase=learning_rate WINNER trial=0 metric=0.3 params={'optimizer.lr': 0.0003}
121
+ ```
122
+
123
+ The `depth` winner's `model.n_layers: 8` is injected into every `learning_rate` trial's complete YAML. Winners persist in the working directory, so you can inspect them any time (abridged):
124
+
125
+ ```bash
126
+ phasesweep show-winners experiment.yaml
127
+ ```
128
+
129
+ ```yaml
130
+ phase: learning_rate
131
+ metric:
132
+ eval_loss: 0.3
133
+ goal: minimize
134
+ trial_number: 0
135
+ params:
136
+ optimizer.lr: 0.0003
137
+ effective_overrides:
138
+ model.n_layers: 8
139
+ optimizer.lr: 0.0003
140
+ # ... completion state, fingerprints, objective provenance, and confound
141
+ # verdicts on the ranked population follow
142
+ ```
143
+
144
+ The starter uses `storage: auto`, so its ledger and outputs live together under
145
+ `<workdir>/<experiment>/`. The ledger is an Optuna journal: `phasesweep status`
146
+ prints its path, and Optuna's own CLI and dashboard can
147
+ [read it](https://github.com/pszemraj/phasesweep/blob/main/docs/runtime.md#inspect-the-ledger-with-optuna). See
148
+ [runtime behavior](https://github.com/pszemraj/phasesweep/blob/main/docs/runtime.md) for state layout, current-format
149
+ requirements, locks, cleanup, GPU isolation, and resume.
150
+
151
+ ## Use your own trainer
152
+
153
+ Choose the input interface your trainer already accepts:
154
+
155
+ | Input | Command placeholder |
156
+ | --- | --- |
157
+ | Complete base-plus-overrides YAML (`yaml_file`, default) | `--config {config_path}` |
158
+ | Command-line flags (`argparse`) | `{overrides}` produces `--key=value` |
159
+ | Native Hydra overrides (`hydra`) | `{overrides}` produces `key=value` |
160
+ | Nested overrides-only JSON (`json_file`) | `--overrides {overrides_path}` |
161
+
162
+ > [!TIP]
163
+ > `phasesweep run experiment.yaml --dry-run` prints one rendered command per
164
+ > phase without launching work. Use it to check that your placeholders render
165
+ > what the trainer expects.
166
+
167
+ For ordinary JSON output, pass an output path under `{trial_dir}` to your
168
+ trainer and select it with `extractor: {type: json, path: result.json, key: eval.loss}`.
169
+ For W&B, use `extractor: {type: wandb, entity: YOUR_ENTITY, project: YOUR_PROJECT, metric_key: eval/loss}`,
170
+ have the trainer honor the supplied W&B identity, and install the extra:
171
+
172
+ ```bash
173
+ python -m pip install "phasesweep[wandb]"
174
+ ```
175
+
176
+ `metric.goal` ranks each trial's selected scalar. Log matching selects the
177
+ last observation by default; W&B selects an exact finished-summary key.
178
+ The trainer owns evaluation, early stopping, checkpoint selection, and
179
+ aggregation. Inheritance transfers parameters, not model weights.
180
+
181
+ See the [configuration guide](https://github.com/pszemraj/phasesweep/blob/main/docs/config.md) for input types, reporting,
182
+ credentials, and selection details. The [Tiny Decoder Enwik8
183
+ example](https://github.com/pszemraj/phasesweep/blob/main/examples/tiny_decoder_enwik8/README.md) is a complete real-trainer
184
+ integration.
185
+
186
+ ## Connect an agent
187
+
188
+ The optional MCP server connects an AI agent to experiments you have approved without exposing config or storage paths, trainer commands, environment values, or raw logs. Sampled winner values follow the catalog's `visible_params` policy. Follow the [MCP setup](https://github.com/pszemraj/phasesweep/blob/main/docs/mcp_setup.md) to install the extra, review the catalog authority boundary, configure a client-owned connection, and verify the result.
189
+
190
+ ## Reference
191
+
192
+ - [Configuration guide](https://github.com/pszemraj/phasesweep/blob/main/docs/config.md): Experiment YAML, search spaces, inheritance, gates, and objective extractors.
193
+ - [Configuration reference](https://github.com/pszemraj/phasesweep/blob/main/docs/config_reference.yaml): per-key types, defaults, valid values, interactions, and lifecycle warnings.
194
+ - [Runtime behavior](https://github.com/pszemraj/phasesweep/blob/main/docs/runtime.md): filesystem layout, locks, GPU leases, process supervision, fingerprints, and resume.
195
+ - [MCP setup](https://github.com/pszemraj/phasesweep/blob/main/docs/mcp_setup.md): installed-package agent onboarding and client-owned setup.
196
+ - [MCP operator reference](https://github.com/pszemraj/phasesweep/blob/main/docs/mcp.md): catalog fields, tools, authorization, run state, and recovery.
197
+ - [Toy experiment and MCP catalog](https://github.com/pszemraj/phasesweep/blob/main/examples/experiment.yaml): a checkout-local CLI example backed by the packaged fake trainer, plus an [MCP catalog](https://github.com/pszemraj/phasesweep/blob/main/examples/catalog.yaml) whose detached-run state and experiment outputs use absolute scratch paths under `/tmp`.
198
+ - [Tiny Decoder Enwik8 example](https://github.com/pszemraj/phasesweep/blob/main/examples/tiny_decoder_enwik8/README.md): real-trainer integration.
199
+ - [Durability invariants](https://github.com/pszemraj/phasesweep/blob/main/docs/invariants.md): the ordering rules the ledger, publication, and recovery paths must hold, and the test that owns each one.
200
+ - [Development](https://github.com/pszemraj/phasesweep/blob/main/docs/development.md): source checkout, contributor setup, and quality gates.
201
+
202
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pszemraj/phasesweep)
203
+
204
+ ## License
205
+
206
+ MIT. See [LICENSE](https://github.com/pszemraj/phasesweep/blob/main/LICENSE).