beqforge 0.2.1__tar.gz → 0.2.3__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 (200) hide show
  1. {beqforge-0.2.1 → beqforge-0.2.3}/.github/workflows/build-device-check.yml +1 -1
  2. {beqforge-0.2.1 → beqforge-0.2.3}/.github/workflows/build-executable.yml +5 -3
  3. {beqforge-0.2.1 → beqforge-0.2.3}/.github/workflows/tests.yml +25 -5
  4. {beqforge-0.2.1 → beqforge-0.2.3}/AGENTS.md +3 -1
  5. {beqforge-0.2.1 → beqforge-0.2.3}/PKG-INFO +4 -3
  6. {beqforge-0.2.1 → beqforge-0.2.3}/README.md +3 -2
  7. {beqforge-0.2.1 → beqforge-0.2.3}/TODO.md +25 -23
  8. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/__init__.py +1 -1
  9. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/AGENTS.md +6 -2
  10. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/diagnose.py +10 -4
  11. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/pipeline.py +26 -3
  12. {beqforge-0.2.1 → beqforge-0.2.3}/plans/done-design-and-pipeline.md +70 -0
  13. beqforge-0.2.3/plans/texture-yardstick-sub-band.md +215 -0
  14. beqforge-0.2.3/plans/texture-yardstick-sub-band.patch +154 -0
  15. {beqforge-0.2.1 → beqforge-0.2.3}/pyproject.toml +18 -1
  16. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_channel_evidence.py +3 -1
  17. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_designer_server.py +81 -48
  18. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_diagnose.py +30 -0
  19. beqforge-0.2.3/tests/test_design_ripple_band.py +56 -0
  20. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_designer_image.py +8 -11
  21. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_designer_server_startup.py +17 -18
  22. {beqforge-0.2.1 → beqforge-0.2.3}/tools/designer_server.py +268 -59
  23. {beqforge-0.2.1 → beqforge-0.2.3}/uv.lock +136 -76
  24. {beqforge-0.2.1 → beqforge-0.2.3}/.dockerignore +0 -0
  25. {beqforge-0.2.1 → beqforge-0.2.3}/.github/dependabot.yml +0 -0
  26. {beqforge-0.2.1 → beqforge-0.2.3}/.github/workflows/build-designer-image.yml +0 -0
  27. {beqforge-0.2.1 → beqforge-0.2.3}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  28. {beqforge-0.2.1 → beqforge-0.2.3}/.gitignore +0 -0
  29. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/.gitignore +0 -0
  30. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/beqforge.iml +0 -0
  31. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/editor.xml +0 -0
  32. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/jsonSchemas.xml +0 -0
  33. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/misc.xml +0 -0
  34. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/modules.xml +0 -0
  35. {beqforge-0.2.1 → beqforge-0.2.3}/.idea/vcs.xml +0 -0
  36. {beqforge-0.2.1 → beqforge-0.2.3}/CLAUDE.md +0 -0
  37. {beqforge-0.2.1 → beqforge-0.2.3}/LICENCE.md +0 -0
  38. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/AGENTS.md +0 -0
  39. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/biquad.py +0 -0
  40. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/provenance.py +0 -0
  41. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/publication.py +0 -0
  42. {beqforge-0.2.1 → beqforge-0.2.3}/beq_common/types.py +0 -0
  43. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/__init__.py +0 -0
  44. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/accept.py +0 -0
  45. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/beqd.py +0 -0
  46. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/biquad.py +0 -0
  47. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/cache.py +0 -0
  48. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/charts.py +0 -0
  49. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/cli.py +0 -0
  50. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/design.py +0 -0
  51. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/designer.py +0 -0
  52. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/explain.py +0 -0
  53. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/extraction.py +0 -0
  54. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/filters.py +0 -0
  55. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/harness.py +0 -0
  56. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/identify.py +0 -0
  57. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/material.py +0 -0
  58. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/record.py +0 -0
  59. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/reference.py +0 -0
  60. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/rolloff.py +0 -0
  61. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge/verify.py +0 -0
  62. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge-device-check.spec +0 -0
  63. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge.spec +0 -0
  64. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/AGENTS.md +0 -0
  65. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/__init__.py +0 -0
  66. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/analyse.py +0 -0
  67. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/audio.py +0 -0
  68. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/catalogue.py +0 -0
  69. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/cli.py +0 -0
  70. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/coefficients.py +0 -0
  71. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/engines.py +0 -0
  72. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/evidence.py +0 -0
  73. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/manifest.py +0 -0
  74. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/measurement.py +0 -0
  75. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/profiles.py +0 -0
  76. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/qualification.py +0 -0
  77. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/report.py +0 -0
  78. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/transactions.py +0 -0
  79. {beqforge-0.2.1 → beqforge-0.2.3}/beqforge_device_check/verify.py +0 -0
  80. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/AGENTS.md +0 -0
  81. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/__init__.py +0 -0
  82. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/cache.py +0 -0
  83. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/cli.py +0 -0
  84. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/core.py +0 -0
  85. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/data/seed-manifest.json +0 -0
  86. {beqforge-0.2.1 → beqforge-0.2.3}/beqoptimiser/data/seed.json.gz +0 -0
  87. {beqforge-0.2.1 → beqforge-0.2.3}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  88. {beqforge-0.2.1 → beqforge-0.2.3}/docs/device-check-examples/simulation.json +0 -0
  89. {beqforge-0.2.1 → beqforge-0.2.3}/docs/device-check.md +0 -0
  90. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/README.md +0 -0
  91. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  92. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  93. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/assets/example-1.png +0 -0
  94. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/assets/example-2.png +0 -0
  95. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/entry-results.json.gz +0 -0
  96. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser-report/statistics.json +0 -0
  97. {beqforge-0.2.1 → beqforge-0.2.3}/docs/optimiser.md +0 -0
  98. {beqforge-0.2.1 → beqforge-0.2.3}/evidence_validation.json +0 -0
  99. {beqforge-0.2.1 → beqforge-0.2.3}/negative_corpus.json +0 -0
  100. {beqforge-0.2.1 → beqforge-0.2.3}/packaging/designer/Dockerfile +0 -0
  101. {beqforge-0.2.1 → beqforge-0.2.3}/packaging/designer/smoke.py +0 -0
  102. {beqforge-0.2.1 → beqforge-0.2.3}/packaging/device-check/NOTICE.txt +0 -0
  103. {beqforge-0.2.1 → beqforge-0.2.3}/plans/F2-catalogue-filter-optimisation.md +0 -0
  104. {beqforge-0.2.1 → beqforge-0.2.3}/plans/F2-device-precision-validation.md +0 -0
  105. {beqforge-0.2.1 → beqforge-0.2.3}/plans/F2-package-consolidation.md +0 -0
  106. {beqforge-0.2.1 → beqforge-0.2.3}/plans/R2a-server-stage-cache.md +0 -0
  107. {beqforge-0.2.1 → beqforge-0.2.3}/plans/done-baseline-evidence.md +0 -0
  108. {beqforge-0.2.1 → beqforge-0.2.3}/plans/research-design-decisions.md +0 -0
  109. {beqforge-0.2.1 → beqforge-0.2.3}/plans/test-portability-and-runtime.md +0 -0
  110. {beqforge-0.2.1 → beqforge-0.2.3}/tests/__init__.py +0 -0
  111. {beqforge-0.2.1 → beqforge-0.2.3}/tests/conftest.py +0 -0
  112. {beqforge-0.2.1 → beqforge-0.2.3}/tests/fixtures/optimiser_seed_entry.json +0 -0
  113. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_cli.py +0 -0
  114. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_accept.py +0 -0
  115. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_cache.py +0 -0
  116. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_cache_stores.py +0 -0
  117. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_charts.py +0 -0
  118. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_corpus.py +0 -0
  119. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_design.py +0 -0
  120. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_designer.py +0 -0
  121. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_device_rate.py +0 -0
  122. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_evidence.py +0 -0
  123. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_evidence_excess.py +0 -0
  124. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_exclusions.py +0 -0
  125. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_explain.py +0 -0
  126. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_extraction.py +0 -0
  127. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_filters.py +0 -0
  128. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_frozen_cache.py +0 -0
  129. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_guard.py +0 -0
  130. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_harness.py +0 -0
  131. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_headroom.py +0 -0
  132. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_hum.py +0 -0
  133. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_identify.py +0 -0
  134. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_jacobian.py +0 -0
  135. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_parallel.py +0 -0
  136. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_pipeline.py +0 -0
  137. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_playback.py +0 -0
  138. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_playback_headroom.py +0 -0
  139. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_publication.py +0 -0
  140. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_record.py +0 -0
  141. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_record_fingerprint.py +0 -0
  142. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_reference.py +0 -0
  143. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_references.py +0 -0
  144. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_rolloff.py +0 -0
  145. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_strategy_config.py +0 -0
  146. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_timings.py +0 -0
  147. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_design_verify.py +0 -0
  148. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_analyse.py +0 -0
  149. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_catalogue.py +0 -0
  150. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_catalogue_predict.py +0 -0
  151. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_cli.py +0 -0
  152. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_engines.py +0 -0
  153. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_evidence.py +0 -0
  154. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_examples.py +0 -0
  155. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_manifest.py +0 -0
  156. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_measurement.py +0 -0
  157. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_qualification.py +0 -0
  158. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_transactions.py +0 -0
  159. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_device_check_verify.py +0 -0
  160. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_extract_layout.py +0 -0
  161. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_optimiser.py +0 -0
  162. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_optimiser_cache.py +0 -0
  163. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_optimiser_report.py +0 -0
  164. {beqforge-0.2.1 → beqforge-0.2.3}/tests/test_package_boundaries.py +0 -0
  165. {beqforge-0.2.1 → beqforge-0.2.3}/tools/__init__.py +0 -0
  166. {beqforge-0.2.1 → beqforge-0.2.3}/tools/build_optimiser_seed.py +0 -0
  167. {beqforge-0.2.1 → beqforge-0.2.3}/tools/design_beq.py +0 -0
  168. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/a1_sweep.py +0 -0
  169. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/compare_records.py +0 -0
  170. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/compare_verdicts.py +0 -0
  171. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/inject_variants.py +0 -0
  172. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p14_compare.py +0 -0
  173. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p14_jacobian.py +0 -0
  174. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p14_run_pipeline.py +0 -0
  175. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p14_surrogate.py +0 -0
  176. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_compare_greedy.py +0 -0
  177. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_compare_seeded.py +0 -0
  178. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_compare_structure.py +0 -0
  179. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_greedy.py +0 -0
  180. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_seeded.py +0 -0
  181. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p18_structure.py +0 -0
  182. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/p20_smooth_drift.py +0 -0
  183. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/probe.py +0 -0
  184. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/refit_probe.py +0 -0
  185. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/request_timing.py +0 -0
  186. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/score_injected.py +0 -0
  187. {beqforge-0.2.1 → beqforge-0.2.3}/tools/experiments/t8_floor_rules.py +0 -0
  188. {beqforge-0.2.1 → beqforge-0.2.3}/tools/extract.py +0 -0
  189. {beqforge-0.2.1 → beqforge-0.2.3}/tools/ledger_template.html +0 -0
  190. {beqforge-0.2.1 → beqforge-0.2.3}/tools/negative_corpus.py +0 -0
  191. {beqforge-0.2.1 → beqforge-0.2.3}/tools/optimiser_catalogue_report.py +0 -0
  192. {beqforge-0.2.1 → beqforge-0.2.3}/tools/prepare_device_check_helper.py +0 -0
  193. {beqforge-0.2.1 → beqforge-0.2.3}/tools/render_ledger.py +0 -0
  194. {beqforge-0.2.1 → beqforge-0.2.3}/tools/replay.py +0 -0
  195. {beqforge-0.2.1 → beqforge-0.2.3}/tools/seed_optimiser_cache.py +0 -0
  196. {beqforge-0.2.1 → beqforge-0.2.3}/tools/smoke_test_device_check.py +0 -0
  197. {beqforge-0.2.1 → beqforge-0.2.3}/tools/smoke_test_exe.py +0 -0
  198. {beqforge-0.2.1 → beqforge-0.2.3}/tools/smoke_test_install.py +0 -0
  199. {beqforge-0.2.1 → beqforge-0.2.3}/tools/summarise.py +0 -0
  200. {beqforge-0.2.1 → beqforge-0.2.3}/tools/validate_evidence.py +0 -0
@@ -61,7 +61,7 @@ jobs:
61
61
  executable: beqforge-device-check
62
62
  steps:
63
63
  - uses: actions/checkout@v7
64
- - uses: astral-sh/setup-uv@v10.2.0
64
+ - uses: astral-sh/setup-uv@v10.3.0
65
65
  with:
66
66
  enable-cache: true
67
67
  - name: Install Linux audio and USB libraries
@@ -25,7 +25,7 @@ jobs:
25
25
  steps:
26
26
  - name: Find successful tests for the tagged commit
27
27
  id: check
28
- uses: actions/github-script@v8
28
+ uses: actions/github-script@v9
29
29
  with:
30
30
  script: |
31
31
  core.setOutput('reusable', 'false');
@@ -72,6 +72,8 @@ jobs:
72
72
  needs: previous_tests
73
73
  if: needs.previous_tests.outputs.reusable != 'true'
74
74
  uses: ./.github/workflows/tests.yml
75
+ # CODECOV_TOKEN, for the coverage upload
76
+ secrets: inherit
75
77
  permissions:
76
78
  contents: read
77
79
 
@@ -102,7 +104,7 @@ jobs:
102
104
  contents: read
103
105
  steps:
104
106
  - uses: actions/checkout@v7
105
- - uses: astral-sh/setup-uv@v10.2.0
107
+ - uses: astral-sh/setup-uv@v10.3.0
106
108
  with:
107
109
  enable-cache: true
108
110
  - name: Build wheel and source distribution
@@ -191,7 +193,7 @@ jobs:
191
193
  - uses: actions/checkout@v7
192
194
 
193
195
  - name: Install uv
194
- uses: astral-sh/setup-uv@v10.2.0
196
+ uses: astral-sh/setup-uv@v10.3.0
195
197
  with:
196
198
  enable-cache: true
197
199
 
@@ -67,7 +67,7 @@ jobs:
67
67
  run: choco install ffmpeg -y
68
68
 
69
69
  - name: Install uv
70
- uses: astral-sh/setup-uv@v10.2.0
70
+ uses: astral-sh/setup-uv@v10.3.0
71
71
  with:
72
72
  enable-cache: true
73
73
 
@@ -76,7 +76,17 @@ jobs:
76
76
 
77
77
  # the device-check tests run in their own job below, in parallel, with the extra installed
78
78
  - name: Run tests
79
- run: uv run --extra designer pytest -n auto --durations=25 --ignore-glob='tests/test_device_check_*.py'
79
+ run: uv run --extra designer pytest -n auto --durations=25 --ignore-glob='tests/test_device_check_*.py' --cov --cov-report=xml
80
+
81
+ - name: Upload coverage
82
+ if: ${{ !cancelled() }}
83
+ uses: codecov/codecov-action@v7.1.1
84
+ with:
85
+ token: ${{ secrets.CODECOV_TOKEN }}
86
+ files: coverage.xml
87
+ disable_search: true
88
+ flags: designer,${{ runner.os }}
89
+ name: designer-${{ runner.os }}
80
90
 
81
91
  device-check:
82
92
  name: Device check (${{ matrix.os }})
@@ -90,7 +100,7 @@ jobs:
90
100
  - uses: actions/checkout@v7
91
101
 
92
102
  - name: Install uv
93
- uses: astral-sh/setup-uv@v10.2.0
103
+ uses: astral-sh/setup-uv@v10.3.0
94
104
  with:
95
105
  enable-cache: true
96
106
 
@@ -100,7 +110,17 @@ jobs:
100
110
 
101
111
  - name: Run device-check tests
102
112
  shell: bash
103
- run: uv run --extra device-check pytest -n auto --durations=25 tests/test_device_check_*.py
113
+ run: uv run --extra device-check pytest -n auto --durations=25 tests/test_device_check_*.py --cov --cov-report=xml
114
+
115
+ - name: Upload coverage
116
+ if: ${{ !cancelled() }}
117
+ uses: codecov/codecov-action@v7.1.1
118
+ with:
119
+ token: ${{ secrets.CODECOV_TOKEN }}
120
+ files: coverage.xml
121
+ disable_search: true
122
+ flags: device-check,${{ runner.os }}
123
+ name: device-check-${{ runner.os }}
104
124
 
105
125
  package-profiles:
106
126
  name: Install profile (${{ matrix.profile }})
@@ -111,7 +131,7 @@ jobs:
111
131
  profile: [base, designer, optimiser, device-check, all]
112
132
  steps:
113
133
  - uses: actions/checkout@v7
114
- - uses: astral-sh/setup-uv@v10.2.0
134
+ - uses: astral-sh/setup-uv@v10.3.0
115
135
  with:
116
136
  enable-cache: true
117
137
  - name: Build the single distribution
@@ -56,7 +56,9 @@ importing them; use an isolated worktree for simultaneous experiments.
56
56
  The root build configuration includes every workflow and the optimiser's bundled seed in one
57
57
  wheel and source distribution. `.github/workflows/tests.yml` checks isolated dependency
58
58
  profiles and runs the suite in parallel (`pytest -n auto`, so tests must not share mutable state
59
- or fixed ports). Feature branch pushes test on Linux only and a newer push cancels the branch's
59
+ or fixed ports), with coverage (`--cov`, configured in `pyproject.toml`, `multiprocessing`
60
+ children included) uploaded to Codecov by each test job using the `CODECOV_TOKEN` secret;
61
+ release runs inherit it. Feature branch pushes test on Linux only and a newer push cancels the branch's
60
62
  previous run; `main`, tags and manual runs test Linux, macOS and Windows and are never cancelled. `.github/workflows/build-executable.yml` builds release executables and distributions;
61
63
  tag pushes publish to PyPI after validation and successful executable builds. Tags must match
62
64
  the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Audio-derived BEQ design, published filter optimisation and DSP validation
5
5
  License-File: LICENCE.md
6
6
  Requires-Python: <3.15,>=3.13
@@ -270,8 +270,9 @@ The image's defaults, all overridable with the environment variables above:
270
270
 
271
271
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
272
272
  the shared root and the cache: the server checks both with a write at startup. Its
273
- `HEALTHCHECK` asks `/health`. The server answers one request at a time (its fitter forks), so a
274
- caller sending several at once queues them; size the caller's timeout for that.
273
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
274
+ worker process, one at a time, so a caller sending several at once queues them; size the
275
+ caller's timeout for that. `docker stop` stops a design in progress.
275
276
 
276
277
  Build and check it locally from the repository root:
277
278
 
@@ -244,8 +244,9 @@ The image's defaults, all overridable with the environment variables above:
244
244
 
245
245
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
246
246
  the shared root and the cache: the server checks both with a write at startup. Its
247
- `HEALTHCHECK` asks `/health`. The server answers one request at a time (its fitter forks), so a
248
- caller sending several at once queues them; size the caller's timeout for that.
247
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
248
+ worker process, one at a time, so a caller sending several at once queues them; size the
249
+ caller's timeout for that. `docker stop` stops a design in progress.
249
250
 
250
251
  Build and check it locally from the repository root:
251
252
 
@@ -18,29 +18,31 @@ are retained so records and code comments remain traceable. Completed items are
18
18
  | Priority | ID | Status | Value, work and blocker or reason for parking |
19
19
  | --- | --- | --- | --- |
20
20
  | 1 | Playback contract | open | Reconcile the supplied bass-management model with the model actually used for targets, verification and clipping. Only `lpf_fs` is consumed today; check the ignored fields before claiming the listener's clipping cost. Caller propagation is an external subtask. |
21
- | 2 | E10 | open | Predeclare fresh content-goal validation before further tuning or a default change. Existing seeds and corpus have repeatedly informed development; writing the protocol needs no new audio. |
22
- | 3 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
23
- | 4 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
24
- | 5 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
25
- | 6 | F2 / device behaviour | in progress; first hardware pilot measured | Coefficient quantisation does not establish hardware accuracy. Limit changes need measured internal arithmetic/noise/limit cycles on a named device and a capture setup; the [measurement plan](plans/F2-device-precision-validation.md) specifies Python sweep/capture, optional REW cross-checks, software controls, single-entry/full-catalogue analysis and a standalone cross-platform executable. The `beqforge-device-check` preview includes frozen manifests, capture/recovery, CLI-only miniDSP controls (minidsp-rs 0.1.12, batched through `-f`) with float32 transport attribution, staged qualification/convergence, uncertainty-aware filter checks, entry/unique-cascade frequency distributions and matched-device deltas. First verified pilot on a miniDSP 2x4 HD (USB loopback, 96 kHz): the device plays its float32 coefficients as predicted (worst 0.0074 dB, 2-200 Hz), while float32 coefficients themselves put 18.8 dB of error into a 5 Hz Q 6 peak; see the plan, section 12. First catalogue sample (50 cascades): 49 play as predicted on every bin (worst 0.0082 dB); the 50th was clipped by the device and is to be remeasured (F2a, plan section 13). Next: the `grid` and `boundary` suites on hardware to map where float32 coefficients stop representing low, high-Q filters. No Q-limit change yet. |
26
- | 7 | Validation coverage (former TODO 10) | blocked | Broad real-programme generalisation needs the missing shapes and independently characterised paired releases listed below. Sparse titles exist; their absolute truth and wider sparse coverage remain missing. |
27
- | 8 | Identification | blocked | Generalising `N(f)` and its fit band needs independent, varied source material, especially paired releases. Sensitivity experiments can proceed; programme-only uncertainty cannot identify the cause of a droop. |
28
- | 9 | E4 | parked | No measured failure has been attributed to isolated ceiling holes. Reopen if unevenness or E9 excess is traced to such bins; smoothing may only reduce the raw licence. |
29
- | 10 | F4 follow-up | parked | Both fitter-objective selection spikes lost passes/true positives. Judge several fits only with a demonstrated usable alternative discarded before `_judge`; a smaller residual is insufficient evidence. |
30
- | 11 | F3 follow-up | parked | A fifth section moved no verdict when tried and costs more fitting. Reopen if an evidence-supported target has a demonstrated usable fit beyond four sections and no usable fit within four. |
31
- | 12 | T2 | parked | The steep-variant audit found no swept cap binding a weak winner. Derive caps only when a binding cap demonstrably prevents a better supported result. |
32
- | 13 | Residual (former TODO 5) | open | Correct the reported target error and its actual measurement band without changing fit selection. Primarily audit accuracy; any escalation/objective change is a separate, higher-impact decision. Non-parametric reporting is already allowed upstream. |
33
- | 14 | Authored features | parked | Manual `--exclude` works; no validated automatic detector exists. Reopen with real labelled features and negatives showing that detection preserves legitimate bass. |
34
- | 15 | Dynamic processing (former TODO 11) | parked | No suspected real case has scoped a useful tripwire. Reopen when compression/limiting demonstrably compromises this correction; non-LTI processing alone does not prove every BEQ is invalid. |
35
- | 16 | Tilted passband reference (goal/corpus follow-up) | parked | No real in-sub-band case needs a sloping reference yet; Dossier's abstention was decided. Reopen with one. Goal-aware corpus labels belong to the open E10 protocol work. Nearest so far: 13 Going on 30's mix tilts about −7 dB/oct above 50 Hz (bass is coherent across channels, ~200 Hz is not) and finds its plateau only through the one-sided fallback. That reference reads a 36 dB deficit below 25 Hz; each channel's own plateau reads 30-34 dB. A reference built from the channels would be 2-5 dB less generous there (see plans/done-design-and-pipeline.md, 11 October 2026). |
36
- | 17 | Mixed-channel policy | parked | `channel_scope` is currently an unpopulated optional response field, not a computed `mixed` finding awaiting a gate. Reopen with a case where existing per-channel restoration and common pricing produce an unsuitable mix correction. |
37
- | 18 | Fraction dial (former TODO 7) | parked | No user/title has needed partial licensed restoration as a preference; tilt and tolerance already ship. Reopen with a concrete requested outcome, then price and validate it like every target. |
38
- | 19 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
39
- | 20 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
40
- | 21 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
41
- | 22 | Performance | parked | Further reuse changes cost, not filter accuracy. Reopen after measuring repeated numerical identities or material remaining analysis cost; missed-fit search belongs to F3/F4 instead. |
42
- | 23 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
43
- | 24 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
21
+ | 2 | Texture yardstick band | open (E10 only) | Implemented: the mix's ripple is measured about a cubic over the unchanged band, so the LFE knee is trend. 9 (2009), Beauty and the Beast, Crazy, Stupid, Love and Despicable Me 3 now accept; Carrie stays declined. Probe and protocol unchanged; corpus 0/45 gate pass. Neither width-normalised form separated `broadband/5`. The degree was chosen on corpus and library data, so it needs the fresh E10 validation before it counts as settled; see [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
22
+ | 3 | E10 | open | Predeclare fresh content-goal validation before further tuning or a default change. Existing seeds and corpus have repeatedly informed development; writing the protocol needs no new audio. |
23
+ | 4 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
24
+ | 5 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
25
+ | 6 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
26
+ | 7 | F2 / device behaviour | in progress; first hardware pilot measured | Coefficient quantisation does not establish hardware accuracy. Limit changes need measured internal arithmetic/noise/limit cycles on a named device and a capture setup; the [measurement plan](plans/F2-device-precision-validation.md) specifies Python sweep/capture, optional REW cross-checks, software controls, single-entry/full-catalogue analysis and a standalone cross-platform executable. The `beqforge-device-check` preview includes frozen manifests, capture/recovery, CLI-only miniDSP controls (minidsp-rs 0.1.12, batched through `-f`) with float32 transport attribution, staged qualification/convergence, uncertainty-aware filter checks, entry/unique-cascade frequency distributions and matched-device deltas. First verified pilot on a miniDSP 2x4 HD (USB loopback, 96 kHz): the device plays its float32 coefficients as predicted (worst 0.0074 dB, 2-200 Hz), while float32 coefficients themselves put 18.8 dB of error into a 5 Hz Q 6 peak; see the plan, section 12. First catalogue sample (50 cascades): 49 play as predicted on every bin (worst 0.0082 dB); the 50th was clipped by the device and is to be remeasured (F2a, plan section 13). Next: the `grid` and `boundary` suites on hardware to map where float32 coefficients stop representing low, high-Q filters. No Q-limit change yet. |
27
+ | 8 | Validation coverage (former TODO 10) | blocked | Broad real-programme generalisation needs the missing shapes and independently characterised paired releases listed below. Sparse titles exist; their absolute truth and wider sparse coverage remain missing. |
28
+ | 9 | Identification | blocked | Generalising `N(f)` and its fit band needs independent, varied source material, especially paired releases. Sensitivity experiments can proceed; programme-only uncertainty cannot identify the cause of a droop. |
29
+ | 10 | E4 | parked | No measured failure has been attributed to isolated ceiling holes. Reopen if unevenness or E9 excess is traced to such bins; smoothing may only reduce the raw licence. |
30
+ | 11 | F4 follow-up | parked | Both fitter-objective selection spikes lost passes/true positives. Judge several fits only with a demonstrated usable alternative discarded before `_judge`; a smaller residual is insufficient evidence. |
31
+ | 12 | F3 follow-up | parked | A fifth section moved no verdict when tried and costs more fitting. Reopen if an evidence-supported target has a demonstrated usable fit beyond four sections and no usable fit within four. |
32
+ | 13 | T2 | parked | The steep-variant audit found no swept cap binding a weak winner. Derive caps only when a binding cap demonstrably prevents a better supported result. |
33
+ | 14 | Residual (former TODO 5) | open | Correct the reported target error and its actual measurement band without changing fit selection. Primarily audit accuracy; any escalation/objective change is a separate, higher-impact decision. Non-parametric reporting is already allowed upstream. |
34
+ | 15 | Authored features | parked | Manual `--exclude` works; no validated automatic detector exists. Reopen with real labelled features and negatives showing that detection preserves legitimate bass. |
35
+ | 16 | Dynamic processing (former TODO 11) | parked | No suspected real case has scoped a useful tripwire. Reopen when compression/limiting demonstrably compromises this correction; non-LTI processing alone does not prove every BEQ is invalid. |
36
+ | 17 | Tilted passband reference (goal/corpus follow-up) | parked | No real in-sub-band case needs a sloping reference yet; Dossier's abstention was decided. Reopen with one. Goal-aware corpus labels belong to the open E10 protocol work. Nearest so far: 13 Going on 30's mix tilts about −7 dB/oct above 50 Hz (bass is coherent across channels, ~200 Hz is not) and finds its plateau only through the one-sided fallback. That reference reads a 36 dB deficit below 25 Hz; each channel's own plateau reads 30-34 dB. A reference built from the channels would be 2-5 dB less generous there (see plans/done-design-and-pipeline.md, 11 October 2026). |
37
+ | 18 | Mixed-channel policy | parked | `channel_scope` is currently an unpopulated optional response field, not a computed `mixed` finding awaiting a gate. Reopen with a case where existing per-channel restoration and common pricing produce an unsuitable mix correction. |
38
+ | 19 | Fraction dial (former TODO 7) | parked | No user/title has needed partial licensed restoration as a preference; tilt and tolerance already ship. Reopen with a concrete requested outcome, then price and validate it like every target. |
39
+ | 20 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
40
+ | 21 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
41
+ | 22 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
42
+ | 23 | Performance | parked | Further reuse changes cost, not filter accuracy. Reopen after measuring repeated numerical identities or material remaining analysis cost; missed-fit search belongs to F3/F4 instead. |
43
+ | 24 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
44
+ | 25 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
45
+ | 26 | Texture yardstick retune (library review) | parked | The cubic (row 2) keeps the 200 Hz band only because the sub-band cap admitted one synthetic negative, `broadband/5`: chance unevenness in noise bursts, at a 2.6 dB boost. With 45 gated cases a single case moves the gate's upper bound from 0.079 to 0.118. Carrie is still declined (0.85x about a cubic, 1.33x with the cap). Reopen when the beqdesigner library review (~1,290 titles) is complete. Then retune against its declines: the band edge, the trend degree, and whether the gate weights a false acceptance by its boost. The library holds no known negative, so it can expose wrong declines but not measure false acceptances. Any retune still needs the corpus or a fresh E10 set for that, ideally with negatives built from real programme texture (row 5). See [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
44
46
 
45
47
  ## Review evidence
46
48
 
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.3"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -43,7 +43,7 @@ still holds: no other API, no other service, and the server is a thin transport
43
43
  | `beqforge/explain.py` | The plain-language account a designer response leads with — `found` (reference plateau, which channels carry it and which are silent, down to where there is real bass content, judged band, goal), `correction` (the cascade, then frequency by frequency what was wanted, what the content supports and before → after), `clipping` (the sub-feed peak and how far to turn the sub down, on the request's or the assumed bass management) and `alternatives` (why each other candidate lost). Reads the `Report`; measures and decides nothing. |
44
44
  | `beqforge/designer.py` | The `design(request) -> response` adapter between beqdesigner's `designer-interface.md` v1.1 and `beqforge.pipeline.run` — request/response dataclasses (1.1: every candidate the judge failed goes back in `rejected` with its failures, review only, beside the answer or the decline), wire-format (de)serialisation and the field-by-field mapping onto `Report`/`Candidate`/`Verdict`. Also `validate_response`, an independent re-implementation of beqdesigner's own §3-§5 rules (that repo isn't importable — PyQt6), called by the server before a response ever goes on the wire. Pure and independently testable; no socket. |
45
45
  | `tools/design_beq.py` | **The entry point.** One command, a filter and its reasoning. `--charts DIR` for the pictures; writes a `.run.json.gz` record beside the material unless `--no-record`. |
46
- | `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. Single-threaded (`http.server.HTTPServer`, not `ThreadingHTTPServer`) on purpose: the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock. Costs nothing real — the contract is one synchronous POST per `design()` call. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version` and `shared_root`. |
46
+ | `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in one long-lived worker process started with "spawn" (`_DesignWorker`): the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock, so the process that forks must stay single-threaded — the worker's log records come back over a `SimpleQueue`, not a `Queue`, for that reason. One worker keeps designs serial (the contract is one synchronous POST per `design()` call); a worker that dies answers 500 and is replaced. SIGTERM shuts down like Ctrl-C, stopping the worker's process group, fit pool included. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond, queued); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version` and `shared_root`. |
47
47
  | `tools/replay.py` | Redraw charts and export to beqdesigner from a record — no rerun, no extraction. Refuses on a stale record unless `--force`. |
48
48
  | `tools/extract.py` | ffmpeg → 1 kHz per-channel `.npz`. Requires an explicit supported channel layout; preserves layout provenance. Needs no beqdesigner. |
49
49
  | `tools/summarise.py` | Sanity-check an extraction before using it. |
@@ -333,7 +333,11 @@ least `min_judge_octaves` above the floor — no fixed top (historical review T6
333
333
  (`pipeline.passband_ripple_db`) is left alone: it abstains with `within_programme_ripple`
334
334
  before any strategy runs. That ripple is the title's own yardstick, so there is no constant.
335
335
  Crest to trough rather than a one-sided dip, because the reference sits on the crests. This
336
- is what took the negative corpus from 4 false acceptances in 45 to 0.
336
+ is what took the negative corpus from 4 false acceptances in 45 to 0. The mix's ripple is
337
+ taken about a cubic in log frequency, so an LFE-led mix's knee above the crossover is trend,
338
+ not texture; each channel's is taken about a line. Don't shorten the band instead:
339
+ crest to trough shrinks with bin count, and the 1x boundary was calibrated on the full band
340
+ ([plans/texture-yardstick-sub-band.md](../plans/texture-yardstick-sub-band.md)).
337
341
  * **Steep filters: the default declines them; `judge_from_content_edge` is an opt-in that
338
342
  recovers them and lifts some noise.** Below a steep filter's edge the priced target already
339
343
  falls into the quiet floor with the programme — contrast bounds it — and the shape clauses
@@ -618,15 +618,21 @@ def diagnose(
618
618
  constrain channel restoration and the band over which a filter is judged.
619
619
  """
620
620
  params = params or DiagnoseParams()
621
- freqs, mix_db = mean_spectrum(material.mono_mix, material.fs)
622
- band = (freqs >= params.band_hz[0]) & (freqs <= params.band_hz[1])
623
- freqs, mix_db = freqs[band], mix_db[band]
621
+ spectrum_freqs, mix_spectrum = mean_spectrum(material.mono_mix, material.fs)
622
+ band = (spectrum_freqs >= params.band_hz[0]) & (spectrum_freqs <= params.band_hz[1])
623
+ freqs, mix_db = spectrum_freqs[band], mix_spectrum[band]
624
624
  spectra = {
625
625
  name: mean_spectrum(samples, material.fs)[1]
626
626
  for name, samples in material.channels.items()
627
627
  }
628
628
  shares, share_errors = coherent_shares(material)
629
- _, mix_plateau = plateau_reference(mix_db, freqs, params)
629
+ # The mix plateau is found on the whole spectrum, exactly as `pipeline` finds it, so tracking
630
+ # is measured from the plateau the run judges against. The reference grid ends exactly at the
631
+ # band top: cut off at the last bin below it, a plateau reaching that edge loses its top point
632
+ # and can vanish. Diagnosis then lost the plateau `pipeline` had, measured no tracking at all
633
+ # and reported the band top as where tracking stopped (A Few Good Men: 150.5-200 Hz found by
634
+ # one, none by the other).
635
+ _, mix_plateau = plateau_reference(mix_spectrum, spectrum_freqs, params)
630
636
  from beqforge.extraction import ExtractionParams, extract
631
637
 
632
638
  extraction_params = extraction_params or ExtractionParams()
@@ -772,6 +772,9 @@ MAINS_HZ = (50.0, 60.0)
772
772
  # inside the 50 Hz spacing so it never reaches the next harmonic
773
773
  HUM_NOTCH_HALF_WIDTH_HZ = 2.5
774
774
  HUM_SURROUND_HALF_WIDTH_HZ = 10.0
775
+ # the mix's texture is measured about a cubic in log frequency, so an LFE-led mix's knee above
776
+ # the crossover is trend, not ripple (plans/texture-yardstick-sub-band.md)
777
+ MIX_TEXTURE_TREND_DEGREE = 3
775
778
 
776
779
 
777
780
  def mains_hum_notches(
@@ -815,7 +818,11 @@ def mains_hum_notches(
815
818
 
816
819
 
817
820
  def passband_ripple_db(
818
- material: Material, params: "PipelineParams", noise_floor_hz: float = math.nan
821
+ material: Material,
822
+ params: "PipelineParams",
823
+ noise_floor_hz: float = math.nan,
824
+ *,
825
+ trend_degree: int = 1,
819
826
  ) -> tuple[float, tuple[float, float]] | None:
820
827
  """The programme's own texture where nothing is missing: crest-to-trough, dB, and where.
821
828
 
@@ -823,6 +830,17 @@ def passband_ripple_db(
823
830
  correction — the top of the judged band to the top of the analysis band — as the spread of
824
831
  the curve about its own log-frequency trend, so a tilted passband is not read as ripple.
825
832
 
833
+ The trend is a polynomial of `trend_degree` in log frequency. The mix's texture blocker
834
+ asks for a cubic (`MIX_TEXTURE_TREND_DEGREE`). In an LFE-led mix the level falls away
835
+ where the LFE channel's content ends (60-100 Hz), a knee a straight line cannot remove, so
836
+ it read as ripple and withheld a real deficit: 9 (2009) measured 8.8 dB of "ripple" against
837
+ a 6.2 dB shortfall, 2.2 dB about a cubic. The band is not shortened instead. Crest to
838
+ trough shrinks with the number of bins even on noise, so ending it at the crossover lowered
839
+ every yardstick and admitted a broadband negative on the corpus. A cubic keeps the band the
840
+ 1x boundary was calibrated on and leaves the negatives where they were: at most 0.76x about
841
+ a cubic against 0.77x about a line, with every real and filtered title at 1.08x and up.
842
+ Each channel's own rule (`channels_missing_low_end`) keeps the line.
843
+
826
844
  Crest to trough, not a one-sided dip, because the reference sits near the 90th percentile
827
845
  of the spectrum, on the crests: a shortfall measured from it spans the whole swing. On the
828
846
  negative corpus (IMPROVEMENT_PLAN E2) every unfiltered title's low-end deficit came in
@@ -847,7 +865,7 @@ def passband_ripple_db(
847
865
  return None
848
866
  smooth = smooth_unexcluded(response, freqs, bands, 15, levels=True)[band]
849
867
  octaves = np.log2(freqs[band])
850
- about = smooth - np.polyval(np.polyfit(octaves, smooth, 1), octaves)
868
+ about = smooth - np.polyval(np.polyfit(octaves, smooth, trend_degree), octaves)
851
869
  return float(np.max(about) - np.min(about)), (low, high)
852
870
 
853
871
 
@@ -1550,7 +1568,12 @@ def analyse(
1550
1568
  f"programme (tracking stops at {diagnosis.noise_floor_hz:.1f} Hz), so there "
1551
1569
  "is no bass passband to restore towards; restoration withheld"
1552
1570
  )
1553
- texture = passband_ripple_db(material, params, diagnosis.noise_floor_hz)
1571
+ texture = passband_ripple_db(
1572
+ material,
1573
+ params,
1574
+ diagnosis.noise_floor_hz,
1575
+ trend_degree=MIX_TEXTURE_TREND_DEGREE,
1576
+ )
1554
1577
  low_end = low_end_deficit_db(material, params, diagnosis.noise_floor_hz)
1555
1578
  if texture is not None and low_end is not None:
1556
1579
  ripple, (edge_low, edge_high) = texture
@@ -748,6 +748,76 @@ beside the answer or the decline (probe unchanged; no decision touched).
748
748
  caller's; `beqforge_revision` is already in every response for it.
749
749
 
750
750
 
751
+ ## The designer server answers `/health` during a design (17 October 2026)
752
+
753
+ A deployed image went unhealthy while it designed correctly. `serve-designer` was a plain
754
+ `HTTPServer` running `design()` on its only thread, so a `GET /health` waited behind a 50-110 s
755
+ design; the image's `HEALTHCHECK` gives up after 3 s and three failures mark the container
756
+ unhealthy. It had been single-threaded on purpose: the fitter forks its pool, and forking a
757
+ multi-threaded process risks a deadlock.
758
+
759
+ The server is now a `ThreadingHTTPServer`, and every design runs in one long-lived worker
760
+ process started with "spawn" (`_DesignWorker` in `tools/designer_server.py`). The worker's
761
+ only thread runs the design, so the fitter forks from a single-threaded process as before. Its
762
+ log records come back over a `SimpleQueue` (a `Queue` would start a feeder thread in it) and
763
+ go through the parent's loggers, and the parent waits for a request's records before
764
+ answering. One worker keeps designs serial. A worker that dies answers 500 and is replaced.
765
+ SIGTERM now shuts down as Ctrl-C does (as a container's PID 1 the server used to ignore it),
766
+ and closing stops the worker's process group, fit pool included, rather than wait for a
767
+ design in progress. The timing line gains `queued`. Nothing under `beqforge/` changed, so the
768
+ decision regression probe does not apply.
769
+
770
+ * **Checks.** New tests: `/health` answers within 3 s while a real design runs, and a killed
771
+ worker gives a 500 and then a working replacement. The existing server, image and startup
772
+ tests now build `DesignServer` with `DesignSettings` (51 passed). Against the installed
773
+ `beqforge serve-designer`: `/health` took at most 16 ms during a design, and SIGTERM in the
774
+ middle of one exited in 0.3 s with no worker or fit-pool process left. Before the
775
+ process-group fix, the fit pool was orphaned and kept computing. The image built and
776
+ `packaging/designer/smoke.py` passed, inline and by reference. Full designer suite with
777
+ coverage: 741 passed, 1 skipped. The fork-from-a-threaded-process warnings in it come from
778
+ xdist's own worker threads and appear without these changes.
779
+ * **Coverage.** CI now measures coverage (`pytest-cov`; `[tool.coverage.run]` in
780
+ `pyproject.toml`, with `concurrency = multiprocessing` so the design worker and fit pools
781
+ count on every platform; `patch = fork` is unsupported on Windows). Both test jobs upload
782
+ `coverage.xml` to Codecov with `CODECOV_TOKEN`, and the release workflow passes its secrets
783
+ to the tests it calls.
784
+
785
+
786
+ ## The mix plateau diagnosis tracks from is the pipeline's (17 October 2026)
787
+
788
+ `diagnose` found the mix plateau on the spectrum cut off at the analysis band's top, while
789
+ `pipeline` finds it on the whole spectrum. `plateau_reference`'s grid ends exactly at the band
790
+ top with `right=nan`, so the cut-off copy always lost the top grid point, and a plateau reaching
791
+ that edge could vanish. Diagnosis then took `_temporal_evidence`'s early return (no strata, the
792
+ tracking floor set to the band top), and the pipeline's plateau blocker read that as "nothing
793
+ below it tracks the programme (tracking stops at 200.0 Hz)". The mix plateau is now found on
794
+ the whole spectrum, as the pipeline finds it. Channel plateaus are unchanged.
795
+
796
+ * **Found in the full-library run (beqdesigner).** A Few Good Men (5.1) declined
797
+ `no_usable_plateau`: the pipeline's plateau was 150.5-200 Hz, diagnosis had none, and its
798
+ record showed no strata and a tracking floor of exactly 200.0. `band_tracking` against the
799
+ pipeline's plateau passes every octave band to ~13 Hz (0.53-0.62). Patched, it accepts two
800
+ low shelves (16.8 Hz +5.8 dB, 26.8 Hz +6.5 dB, confidence 0.91). Before the Devil Knows
801
+ You're Dead and Blink Twice show the same record signature.
802
+ * **First version: every plateau on the whole spectrum, rejected.** Handing channels the whole
803
+ spectrum too moved channel plateaus on eight baseline titles (an extra finite top point moves
804
+ the 90th-percentile threshold), changed counterfactual targets on four, and in real runs
805
+ against HEAD flipped counterfactual verdicts on Alto Knights and Obsession. As with the
806
+ one-sided fallback above, the channel calls were left alone.
807
+ * **Validation of the mix-only version**, against 7a1702f. Probe: 28 Years Later's tracking
808
+ floor 20.50 -> 20.30 Hz (judged band and targets by up to 0.17 dB); Dossier 137's filter
809
+ floor 59.3 -> 79.8 Hz and Send Help's 20.0 -> 20.8 Hz, nothing else; the other eight titles
810
+ unchanged. 28 Years Later's real run against a HEAD run: the winner changes `parametric` ->
811
+ `flatten`, both passing either way; the accepted corrections agree within 0.6 dB in every band
812
+ from 5 to 80 Hz (+5.8/+6.0/+6.6/+3.3/+0.6 -> +5.9/+6.6/+6.4/+3.1/+0.7 dB), the same answer.
813
+ Protocol, seeds 101 and 947: identical false acceptances, abstentions, selections and recovery.
814
+ Corpus, seeds 1-9: gated 0/45, gate pass 0.079; positives 7/9 (median recovery 1.27 dB RMS);
815
+ `natural_droop` 4/9 against the committed file's 5/9 (an older revision; reported, not gated).
816
+ Full suite: 804 passed, 2 skipped. New test in `tests/test_design_diagnose.py`: a mix
817
+ high-passed at 144-146 Hz, whose only plateau reaches the band top, is tracked from that
818
+ plateau (fails on the old code: no strata, floor at 200 Hz).
819
+
820
+
751
821
  ## Levels beside an omission, and mains-hum notches as texture (11 October 2026)
752
822
 
753
823
  Three titles from a beqdesigner run against `/media/beq` were declined in ways that should not