beqforge 0.2.4__tar.gz → 0.2.7__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 (211) hide show
  1. {beqforge-0.2.4 → beqforge-0.2.7}/.github/workflows/build-designer-image.yml +6 -3
  2. {beqforge-0.2.4 → beqforge-0.2.7}/.github/workflows/build-executable.yml +40 -20
  3. {beqforge-0.2.4 → beqforge-0.2.7}/AGENTS.md +2 -1
  4. {beqforge-0.2.4 → beqforge-0.2.7}/PKG-INFO +50 -11
  5. {beqforge-0.2.4 → beqforge-0.2.7}/README.md +49 -10
  6. {beqforge-0.2.4 → beqforge-0.2.7}/TODO.md +2 -20
  7. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/__init__.py +1 -1
  8. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/AGENTS.md +17 -6
  9. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/designer.py +203 -9
  10. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/diagnose.py +98 -8
  11. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/explain.py +30 -1
  12. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/pipeline.py +125 -7
  13. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/record.py +6 -0
  14. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/AGENTS.md +5 -1
  15. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/__init__.py +15 -0
  16. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/cache.py +182 -4
  17. beqforge-0.2.7/beqoptimiser/parameters.py +499 -0
  18. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser.md +51 -0
  19. {beqforge-0.2.4 → beqforge-0.2.7}/packaging/designer/beqforge-designer.cmd +6 -0
  20. {beqforge-0.2.4 → beqforge-0.2.7}/packaging/designer/smoke.py +22 -3
  21. beqforge-0.2.7/plans/default-record-and-log-locations.md +46 -0
  22. beqforge-0.2.7/plans/design-version.md +143 -0
  23. {beqforge-0.2.4 → beqforge-0.2.7}/plans/done-design-and-pipeline.md +87 -2
  24. {beqforge-0.2.4 → beqforge-0.2.7}/plans/parameter-space-optimisation.md +65 -1
  25. beqforge-0.2.7/tests/fixtures/design_golden.json +50 -0
  26. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_designer.py +265 -12
  27. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_designer_server.py +42 -3
  28. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_diagnose.py +49 -1
  29. beqforge-0.2.7/tests/test_design_golden.py +232 -0
  30. beqforge-0.2.7/tests/test_design_target_fit.py +112 -0
  31. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_designer_server_startup.py +51 -6
  32. beqforge-0.2.7/tests/test_optimiser_parameters.py +233 -0
  33. {beqforge-0.2.4 → beqforge-0.2.7}/tools/design_beq.py +2 -0
  34. {beqforge-0.2.4 → beqforge-0.2.7}/tools/designer_server.py +73 -20
  35. beqforge-0.2.7/tools/experiments/parameter_benchmark.py +152 -0
  36. {beqforge-0.2.4 → beqforge-0.2.7}/tools/smoke_test_exe.py +20 -0
  37. {beqforge-0.2.4 → beqforge-0.2.7}/.dockerignore +0 -0
  38. {beqforge-0.2.4 → beqforge-0.2.7}/.gitattributes +0 -0
  39. {beqforge-0.2.4 → beqforge-0.2.7}/.github/dependabot.yml +0 -0
  40. {beqforge-0.2.4 → beqforge-0.2.7}/.github/workflows/build-device-check.yml +0 -0
  41. {beqforge-0.2.4 → beqforge-0.2.7}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  42. {beqforge-0.2.4 → beqforge-0.2.7}/.github/workflows/tests.yml +0 -0
  43. {beqforge-0.2.4 → beqforge-0.2.7}/.gitignore +0 -0
  44. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/.gitignore +0 -0
  45. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/beqforge.iml +0 -0
  46. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/editor.xml +0 -0
  47. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/jsonSchemas.xml +0 -0
  48. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/misc.xml +0 -0
  49. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/modules.xml +0 -0
  50. {beqforge-0.2.4 → beqforge-0.2.7}/.idea/vcs.xml +0 -0
  51. {beqforge-0.2.4 → beqforge-0.2.7}/CLAUDE.md +0 -0
  52. {beqforge-0.2.4 → beqforge-0.2.7}/LICENCE.md +0 -0
  53. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/AGENTS.md +0 -0
  54. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/biquad.py +0 -0
  55. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/provenance.py +0 -0
  56. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/publication.py +0 -0
  57. {beqforge-0.2.4 → beqforge-0.2.7}/beq_common/types.py +0 -0
  58. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/__init__.py +0 -0
  59. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/accept.py +0 -0
  60. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/beqd.py +0 -0
  61. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/biquad.py +0 -0
  62. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/cache.py +0 -0
  63. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/charts.py +0 -0
  64. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/cli.py +0 -0
  65. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/design.py +0 -0
  66. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/extraction.py +0 -0
  67. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/filters.py +0 -0
  68. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/harness.py +0 -0
  69. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/identify.py +0 -0
  70. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/material.py +0 -0
  71. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/reference.py +0 -0
  72. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/rolloff.py +0 -0
  73. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge/verify.py +0 -0
  74. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge-device-check.spec +0 -0
  75. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge.spec +0 -0
  76. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/AGENTS.md +0 -0
  77. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/__init__.py +0 -0
  78. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/analyse.py +0 -0
  79. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/audio.py +0 -0
  80. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/catalogue.py +0 -0
  81. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/cli.py +0 -0
  82. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/coefficients.py +0 -0
  83. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/engines.py +0 -0
  84. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/evidence.py +0 -0
  85. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/manifest.py +0 -0
  86. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/measurement.py +0 -0
  87. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/profiles.py +0 -0
  88. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/qualification.py +0 -0
  89. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/report.py +0 -0
  90. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/transactions.py +0 -0
  91. {beqforge-0.2.4 → beqforge-0.2.7}/beqforge_device_check/verify.py +0 -0
  92. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/cli.py +0 -0
  93. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/core.py +0 -0
  94. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/data/seed-manifest.json +0 -0
  95. {beqforge-0.2.4 → beqforge-0.2.7}/beqoptimiser/data/seed.json.gz +0 -0
  96. {beqforge-0.2.4 → beqforge-0.2.7}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  97. {beqforge-0.2.4 → beqforge-0.2.7}/docs/device-check-examples/simulation.json +0 -0
  98. {beqforge-0.2.4 → beqforge-0.2.7}/docs/device-check.md +0 -0
  99. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/README.md +0 -0
  100. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  101. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  102. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/assets/example-1.png +0 -0
  103. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/assets/example-2.png +0 -0
  104. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/entry-results.json.gz +0 -0
  105. {beqforge-0.2.4 → beqforge-0.2.7}/docs/optimiser-report/statistics.json +0 -0
  106. {beqforge-0.2.4 → beqforge-0.2.7}/evidence_validation.json +0 -0
  107. {beqforge-0.2.4 → beqforge-0.2.7}/negative_corpus.json +0 -0
  108. {beqforge-0.2.4 → beqforge-0.2.7}/packaging/designer/Dockerfile +0 -0
  109. {beqforge-0.2.4 → beqforge-0.2.7}/packaging/device-check/NOTICE.txt +0 -0
  110. {beqforge-0.2.4 → beqforge-0.2.7}/plans/F2-catalogue-filter-optimisation.md +0 -0
  111. {beqforge-0.2.4 → beqforge-0.2.7}/plans/F2-device-precision-validation.md +0 -0
  112. {beqforge-0.2.4 → beqforge-0.2.7}/plans/F2-package-consolidation.md +0 -0
  113. {beqforge-0.2.4 → beqforge-0.2.7}/plans/R2a-server-stage-cache.md +0 -0
  114. {beqforge-0.2.4 → beqforge-0.2.7}/plans/done-baseline-evidence.md +0 -0
  115. {beqforge-0.2.4 → beqforge-0.2.7}/plans/research-design-decisions.md +0 -0
  116. {beqforge-0.2.4 → beqforge-0.2.7}/plans/test-portability-and-runtime.md +0 -0
  117. {beqforge-0.2.4 → beqforge-0.2.7}/plans/texture-yardstick-sub-band.md +0 -0
  118. {beqforge-0.2.4 → beqforge-0.2.7}/plans/texture-yardstick-sub-band.patch +0 -0
  119. {beqforge-0.2.4 → beqforge-0.2.7}/pyproject.toml +0 -0
  120. {beqforge-0.2.4 → beqforge-0.2.7}/tests/__init__.py +0 -0
  121. {beqforge-0.2.4 → beqforge-0.2.7}/tests/conftest.py +0 -0
  122. {beqforge-0.2.4 → beqforge-0.2.7}/tests/fixtures/optimiser_seed_entry.json +0 -0
  123. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_cli.py +0 -0
  124. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_accept.py +0 -0
  125. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_cache.py +0 -0
  126. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_cache_stores.py +0 -0
  127. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_channel_evidence.py +0 -0
  128. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_charts.py +0 -0
  129. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_corpus.py +0 -0
  130. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_design.py +0 -0
  131. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_device_rate.py +0 -0
  132. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_evidence.py +0 -0
  133. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_evidence_excess.py +0 -0
  134. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_exclusions.py +0 -0
  135. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_explain.py +0 -0
  136. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_extraction.py +0 -0
  137. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_filters.py +0 -0
  138. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_frozen_cache.py +0 -0
  139. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_guard.py +0 -0
  140. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_harness.py +0 -0
  141. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_headroom.py +0 -0
  142. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_hum.py +0 -0
  143. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_identify.py +0 -0
  144. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_jacobian.py +0 -0
  145. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_parallel.py +0 -0
  146. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_pipeline.py +0 -0
  147. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_playback.py +0 -0
  148. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_playback_headroom.py +0 -0
  149. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_publication.py +0 -0
  150. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_record.py +0 -0
  151. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_record_fingerprint.py +0 -0
  152. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_reference.py +0 -0
  153. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_references.py +0 -0
  154. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_ripple_band.py +0 -0
  155. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_rolloff.py +0 -0
  156. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_strategy_config.py +0 -0
  157. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_timings.py +0 -0
  158. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_design_verify.py +0 -0
  159. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_designer_image.py +0 -0
  160. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_analyse.py +0 -0
  161. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_catalogue.py +0 -0
  162. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_catalogue_predict.py +0 -0
  163. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_cli.py +0 -0
  164. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_engines.py +0 -0
  165. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_evidence.py +0 -0
  166. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_examples.py +0 -0
  167. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_manifest.py +0 -0
  168. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_measurement.py +0 -0
  169. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_qualification.py +0 -0
  170. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_transactions.py +0 -0
  171. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_device_check_verify.py +0 -0
  172. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_extract_layout.py +0 -0
  173. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_optimiser.py +0 -0
  174. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_optimiser_cache.py +0 -0
  175. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_optimiser_report.py +0 -0
  176. {beqforge-0.2.4 → beqforge-0.2.7}/tests/test_package_boundaries.py +0 -0
  177. {beqforge-0.2.4 → beqforge-0.2.7}/tools/__init__.py +0 -0
  178. {beqforge-0.2.4 → beqforge-0.2.7}/tools/build_optimiser_seed.py +0 -0
  179. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/a1_sweep.py +0 -0
  180. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/compare_records.py +0 -0
  181. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/compare_verdicts.py +0 -0
  182. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/inject_variants.py +0 -0
  183. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p14_compare.py +0 -0
  184. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p14_jacobian.py +0 -0
  185. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p14_run_pipeline.py +0 -0
  186. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p14_surrogate.py +0 -0
  187. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_compare_greedy.py +0 -0
  188. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_compare_seeded.py +0 -0
  189. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_compare_structure.py +0 -0
  190. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_greedy.py +0 -0
  191. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_seeded.py +0 -0
  192. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p18_structure.py +0 -0
  193. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/p20_smooth_drift.py +0 -0
  194. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/probe.py +0 -0
  195. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/refit_probe.py +0 -0
  196. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/request_timing.py +0 -0
  197. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/score_injected.py +0 -0
  198. {beqforge-0.2.4 → beqforge-0.2.7}/tools/experiments/t8_floor_rules.py +0 -0
  199. {beqforge-0.2.4 → beqforge-0.2.7}/tools/extract.py +0 -0
  200. {beqforge-0.2.4 → beqforge-0.2.7}/tools/ledger_template.html +0 -0
  201. {beqforge-0.2.4 → beqforge-0.2.7}/tools/negative_corpus.py +0 -0
  202. {beqforge-0.2.4 → beqforge-0.2.7}/tools/optimiser_catalogue_report.py +0 -0
  203. {beqforge-0.2.4 → beqforge-0.2.7}/tools/prepare_device_check_helper.py +0 -0
  204. {beqforge-0.2.4 → beqforge-0.2.7}/tools/render_ledger.py +0 -0
  205. {beqforge-0.2.4 → beqforge-0.2.7}/tools/replay.py +0 -0
  206. {beqforge-0.2.4 → beqforge-0.2.7}/tools/seed_optimiser_cache.py +0 -0
  207. {beqforge-0.2.4 → beqforge-0.2.7}/tools/smoke_test_device_check.py +0 -0
  208. {beqforge-0.2.4 → beqforge-0.2.7}/tools/smoke_test_install.py +0 -0
  209. {beqforge-0.2.4 → beqforge-0.2.7}/tools/summarise.py +0 -0
  210. {beqforge-0.2.4 → beqforge-0.2.7}/tools/validate_evidence.py +0 -0
  211. {beqforge-0.2.4 → beqforge-0.2.7}/uv.lock +0 -0
@@ -1,15 +1,16 @@
1
1
  name: Designer image
2
2
 
3
3
  # The designer as a container, beside a beqdesigner pipeline service (README, "Running the
4
- # designer in a container"). Every change to what goes into it is built and smoke-tested;
5
- # a vX.Y.Z tag also publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer.
4
+ # designer in a container"). Every change to what goes into it is built and smoke-tested.
5
+ # A release publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer:
6
+ # build-executable.yml calls this for a vX.Y.Z tag once the release tests have passed, as it
7
+ # does for the executables and PyPI, so an image never ships ahead of its tests.
6
8
  # A manual run with `publish` publishes the image of an existing release from a later commit,
7
9
  # only if the designer's runtime code is identical to that release's tag (as when the image
8
10
  # recipe arrived after the release).
9
11
  on:
10
12
  push:
11
13
  branches: [main]
12
- tags: ['v*']
13
14
  paths:
14
15
  - 'packaging/designer/**'
15
16
  - '.dockerignore'
@@ -30,6 +31,8 @@ on:
30
31
  description: Publish the image as beq_common.__version__ (its vX.Y.Z tag must exist, with identical designer code)
31
32
  type: boolean
32
33
  default: false
34
+ # the release: build-executable.yml, from a tag, after its test gate
35
+ workflow_call:
33
36
 
34
37
  permissions:
35
38
  contents: read
@@ -34,9 +34,10 @@ jobs:
34
34
  return;
35
35
  }
36
36
  const deadline = Date.now() + 15 * 60 * 1000;
37
- try {
38
- while (true) {
39
- const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
37
+ while (true) {
38
+ let runs;
39
+ try {
40
+ runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
40
41
  ...context.repo,
41
42
  workflow_id: 'tests.yml',
42
43
  head_sha: context.sha,
@@ -44,27 +45,34 @@ jobs:
44
45
  event: 'push',
45
46
  per_page: 100,
46
47
  });
47
- const matching = runs.filter(run =>
48
- run.head_sha === context.sha && run.head_branch === 'main' && run.event === 'push'
49
- );
50
- const passed = matching.find(run =>
51
- run.status === 'completed' && run.conclusion === 'success'
52
- );
53
- if (passed) {
54
- core.info(`Reusing successful main tests: ${passed.html_url}`);
55
- await core.summary.addLink('Reused main test run', passed.html_url).write();
56
- core.setOutput('reusable', 'true');
48
+ } catch (error) {
49
+ // a transient API failure is no reason to rerun the whole suite, so keep trying until the deadline
50
+ if (Date.now() >= deadline) {
51
+ core.warning(`Unable to check main tests; running fresh release tests: ${error.message}`);
57
52
  return;
58
53
  }
59
- if (!matching.some(run => run.status !== 'completed') || Date.now() >= deadline) {
60
- core.info('No successful main test run available; running fresh release tests.');
61
- return;
62
- }
63
- core.info('Main tests are still running; checking again in 30 seconds.');
54
+ core.info(`Unable to check main tests (${error.message}); trying again in 30 seconds.`);
64
55
  await new Promise(resolve => setTimeout(resolve, 30_000));
56
+ continue;
57
+ }
58
+ const matching = runs.filter(run =>
59
+ run.head_sha === context.sha && run.head_branch === 'main' && run.event === 'push'
60
+ );
61
+ const passed = matching.find(run =>
62
+ run.status === 'completed' && run.conclusion === 'success'
63
+ );
64
+ if (passed) {
65
+ core.info(`Reusing successful main tests: ${passed.html_url}`);
66
+ await core.summary.addLink('Reused main test run', passed.html_url).write();
67
+ core.setOutput('reusable', 'true');
68
+ return;
65
69
  }
66
- } catch (error) {
67
- core.warning(`Unable to check main tests; running fresh release tests: ${error.message}`);
70
+ if (!matching.some(run => run.status !== 'completed') || Date.now() >= deadline) {
71
+ core.info('No successful main test run available; running fresh release tests.');
72
+ return;
73
+ }
74
+ core.info('Main tests are still running; checking again in 30 seconds.');
75
+ await new Promise(resolve => setTimeout(resolve, 30_000));
68
76
  }
69
77
 
70
78
  tests:
@@ -234,6 +242,18 @@ jobs:
234
242
  env:
235
243
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
236
244
 
245
+ image:
246
+ name: Publish designer image
247
+ needs: [validated_tests, package]
248
+ # see create_release for why the needed results are checked explicitly
249
+ if: >-
250
+ !cancelled() && startsWith(github.ref, 'refs/tags/') &&
251
+ needs.validated_tests.result == 'success' && needs.package.result == 'success'
252
+ uses: ./.github/workflows/build-designer-image.yml
253
+ permissions:
254
+ contents: read
255
+ packages: write
256
+
237
257
  publish:
238
258
  name: Publish Python package to PyPI
239
259
  needs: [validated_tests, package, build, create_release]
@@ -60,7 +60,8 @@ or fixed ports), with coverage (`--cov`, configured in `pyproject.toml`, `multip
60
60
  children included) uploaded to Codecov by each test job using the `CODECOV_TOKEN` secret;
61
61
  release runs inherit it. Feature branch pushes test on Linux only and a newer push cancels the branch's
62
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;
63
- tag pushes publish to PyPI after validation and successful executable builds. Tags must match
63
+ tag pushes publish to PyPI after validation and successful executable builds, and the designer
64
+ image to GHCR after validation. Tags must match
64
65
  the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
65
66
  PyPI uses the `pypi` environment and Trusted Publishing; setup details are in the README.
66
67
  Device-check preview executable packaging has its own workflow, not a separate Python release;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.4
3
+ Version: 0.2.7
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
@@ -146,7 +146,7 @@ straight off disk. A stale record is skipped with a warning rather than failing
146
146
 
147
147
  `beqforge serve-designer` runs this pipeline as a live, HTTP-bound filter designer for
148
148
  [`beqdesigner`](https://github.com/3ll3d00d/beqdesigner), implementing its
149
- `design/designer-interface.md` v1.2 contract (§7.1's HTTP binding) rather than the file-based
149
+ `design/designer-interface.md` v1.4 contract (§7.1's HTTP binding) rather than the file-based
150
150
  `.npz` → `design` → `--beq` export flow above:
151
151
 
152
152
  ```bash
@@ -183,15 +183,19 @@ An unusable directory causes a startup error naming the option and path.
183
183
  relative to that root plus the SHA-256 of its decoded column, instead of inline base64 audio.
184
184
  The server resolves, decodes and checks each reference; an unusable reference gets HTTP 422
185
185
  naming the array, rather than a design decline. Inline requests remain supported.
186
- `GET /health` reports `contract_version` and whether `shared_root` is enabled. Each request
186
+ `GET /health` reports `contract_version`, whether `shared_root` is enabled, and 1.4's design
187
+ version, settings and build. Each request
187
188
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
188
189
  measures cold and warm requests.
189
190
 
190
191
  **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
191
192
  its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
192
193
  caller can send each server as many designs as it takes rather than be configured with that
193
- number. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
194
- if set, stays each design's own). The share changes how long a fit takes, never its answer.
194
+ number. Concurrent designs share the machine's logical processors, SMT threads included, each
195
+ at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
196
+ `--cores N` caps the processors the designs use between them, for a machine with other work to
197
+ do: concurrent designs split N evenly, and N must be at least `--workers`. The share changes
198
+ how long a fit takes, never its answer.
195
199
 
196
200
  Every `serve-designer` configuration option can also be set through an environment variable.
197
201
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
@@ -214,6 +218,7 @@ their existing CLI configuration.
214
218
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
215
219
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
216
220
  | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
221
+ | `--cores` | `BEQFORGE_CORES` | integer, at least `--workers` |
217
222
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
218
223
 
219
224
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -246,21 +251,51 @@ diagnosis, in this order:
246
251
  carried `bass_management`, otherwise the assumed LR4 80 Hz model, and it says which).
247
252
  Clipping is reported, never used to reject a filter. The typed `gain_reduction_db` field is
248
253
  filled only when the request carries `bass_management`, as the contract requires.
254
+ * `fit` — how closely the filter, as the device plays it, delivers the boost the content
255
+ supports: the typical error, the share of that boost within ±0.5 dB, and the worst miss,
256
+ where and which way. It is a match to that target, not a verdict on whether the target is
257
+ right; `correction` says how much of what is missing that target restores.
249
258
  * `alternatives` — why each other candidate was rejected or not chosen.
250
259
 
251
260
  The commentary then continues with `target_notes`, `verdict_notes` (what the checks themselves
252
- noticed), `effective_params` and `beqforge_revision` (which build answered). The raw
261
+ noticed), `effective_params` and `beqforge_revision` (which build answered). The same fit is
262
+ in typed fields (contract 1.3): `residual_db` is the worst absolute error over
263
+ `residual_band_hz`, the band the fit was actually scored on, measured on the published filter
264
+ as the device plays it; `residual_at_hz` and `residual_direction` say where and which way;
265
+ `residual_rms_db` and `residual_within_tolerance` (against `residual_tolerance_db`) weight
266
+ each frequency by the boost asked for there, as `confidence` does. The raw
253
267
  recovered and shaping fractions are kept in the run record, not the response. A decline's `decline_message` gives the reason first, then `| found: …` with
254
268
  the same diagnosis, then the build in brackets. `beqforge/designer.py` is the
255
269
  pure `design(request) -> response` adapter, independently testable without a socket;
256
270
  `tools/designer_server.py` is the HTTP transport around it.
257
271
 
272
+ **Which answer a build gives (contract 1.4).** Every response, design or decline, and
273
+ `GET /health` carry `design_version`, `design_settings` and `designer_build`, whatever
274
+ `contract_version` the request names. `design_version` is an integer bumped only by a change
275
+ that can alter an answer for an unchanged request: the candidates, the rejected designs, a
276
+ decline or a fit figure. Wording, speed and
277
+ packaging don't bump it, so the executable and the image of one release report the same
278
+ version and different `designer_build`s (the string also in `beqforge_revision`). Releases
279
+ before it: 0.0.x and 0.1.0 are 0, 0.2.0 is 1, 0.2.1 is 2, 0.2.2 is 3, and 0.2.3 to 0.2.5
280
+ are 4, as is 0.2.7, the first release to report it. One version is one algorithm, not one
281
+ filter on every machine: the `counterfactual` and `parametric` fits can land on a different
282
+ filter on a different processor, so compare versions, never figures. `design_settings` is a 12-hex digest of the server-wide settings that can change an
283
+ answer (`--strategy`, `--exclude`, `--goal-tolerance`, `--goal-tilt`, `--content-edge`,
284
+ `--device-rate`, `--coefficient-bits`, `--integer-bits`) that differ from their defaults, so a
285
+ server started with none of them always sends the same digest. `/health` also reports those
286
+ settings as `design_settings_detail`, for reading when two designers disagree. A caller keys
287
+ each title on (`design_version`, `design_settings`) to tell which titles a new release would
288
+ answer differently.
289
+
258
290
  ### Running the designer in a container
259
291
 
260
292
  `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
261
293
  pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
262
- published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
294
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
263
295
  each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
296
+ The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
297
+ as it does for the executables and PyPI; pushes to `main` and pull requests build and
298
+ smoke-test the image without publishing it.
264
299
  A manual run of that workflow with `publish` set publishes the current version's image from a
265
300
  later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
266
301
  image was published this way, the recipe having arrived after the release).
@@ -304,8 +339,10 @@ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
304
339
  ```
305
340
 
306
341
  The launcher sets the image's defaults through the environment variables above, each only if
307
- not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
308
- `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
342
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420`,
343
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache` and
344
+ `BEQFORGE_RECORD_DIR=%LOCALAPPDATA%\beqforge\records`, so every request's run record is kept.
345
+ Arguments pass through to `serve-designer`.
309
346
 
310
347
  * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
311
348
  reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
@@ -317,7 +354,8 @@ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
317
354
  `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
318
355
  Check `http://<address>:8420/health` from there first.
319
356
  * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
320
- design more than one title at a time; `/health` reports it as `parallel`.
357
+ design more than one title at a time; `/health` reports it as `parallel`. To leave room for
358
+ other work, set `BEQFORGE_CORES` to the most processors the designer may use between them.
321
359
  * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
322
360
  press Ctrl+C, to stop.
323
361
 
@@ -334,7 +372,8 @@ one locally: `uv pip install pyinstaller && uv run pyinstaller beqforge.spec`.
334
372
 
335
373
  The same workflow builds and checks the Python wheel and source distribution, verifies all
336
374
  five dependency profiles, and reuses a successful branch-push run of `tests.yml` on `main`
337
- at the exact tagged commit. If matching tests are still running, it waits up to 15 minutes;
375
+ at the exact tagged commit. If matching tests are still running, or GitHub's API fails to answer, it
376
+ keeps checking for up to 15 minutes;
338
377
  without a successful result, it runs the reusable tests workflow against the release commit.
339
378
  GitHub release creation and executable builds require those tests to pass on Linux, macOS
340
379
  and Windows, including device-check tests and isolated dependency profiles. PyPI publication
@@ -120,7 +120,7 @@ straight off disk. A stale record is skipped with a warning rather than failing
120
120
 
121
121
  `beqforge serve-designer` runs this pipeline as a live, HTTP-bound filter designer for
122
122
  [`beqdesigner`](https://github.com/3ll3d00d/beqdesigner), implementing its
123
- `design/designer-interface.md` v1.2 contract (§7.1's HTTP binding) rather than the file-based
123
+ `design/designer-interface.md` v1.4 contract (§7.1's HTTP binding) rather than the file-based
124
124
  `.npz` → `design` → `--beq` export flow above:
125
125
 
126
126
  ```bash
@@ -157,15 +157,19 @@ An unusable directory causes a startup error naming the option and path.
157
157
  relative to that root plus the SHA-256 of its decoded column, instead of inline base64 audio.
158
158
  The server resolves, decodes and checks each reference; an unusable reference gets HTTP 422
159
159
  naming the array, rather than a design decline. Inline requests remain supported.
160
- `GET /health` reports `contract_version` and whether `shared_root` is enabled. Each request
160
+ `GET /health` reports `contract_version`, whether `shared_root` is enabled, and 1.4's design
161
+ version, settings and build. Each request
161
162
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
162
163
  measures cold and warm requests.
163
164
 
164
165
  **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
165
166
  its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
166
167
  caller can send each server as many designs as it takes rather than be configured with that
167
- number. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
168
- if set, stays each design's own). The share changes how long a fit takes, never its answer.
168
+ number. Concurrent designs share the machine's logical processors, SMT threads included, each
169
+ at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
170
+ `--cores N` caps the processors the designs use between them, for a machine with other work to
171
+ do: concurrent designs split N evenly, and N must be at least `--workers`. The share changes
172
+ how long a fit takes, never its answer.
169
173
 
170
174
  Every `serve-designer` configuration option can also be set through an environment variable.
171
175
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
@@ -188,6 +192,7 @@ their existing CLI configuration.
188
192
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
189
193
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
190
194
  | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
195
+ | `--cores` | `BEQFORGE_CORES` | integer, at least `--workers` |
191
196
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
192
197
 
193
198
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -220,21 +225,51 @@ diagnosis, in this order:
220
225
  carried `bass_management`, otherwise the assumed LR4 80 Hz model, and it says which).
221
226
  Clipping is reported, never used to reject a filter. The typed `gain_reduction_db` field is
222
227
  filled only when the request carries `bass_management`, as the contract requires.
228
+ * `fit` — how closely the filter, as the device plays it, delivers the boost the content
229
+ supports: the typical error, the share of that boost within ±0.5 dB, and the worst miss,
230
+ where and which way. It is a match to that target, not a verdict on whether the target is
231
+ right; `correction` says how much of what is missing that target restores.
223
232
  * `alternatives` — why each other candidate was rejected or not chosen.
224
233
 
225
234
  The commentary then continues with `target_notes`, `verdict_notes` (what the checks themselves
226
- noticed), `effective_params` and `beqforge_revision` (which build answered). The raw
235
+ noticed), `effective_params` and `beqforge_revision` (which build answered). The same fit is
236
+ in typed fields (contract 1.3): `residual_db` is the worst absolute error over
237
+ `residual_band_hz`, the band the fit was actually scored on, measured on the published filter
238
+ as the device plays it; `residual_at_hz` and `residual_direction` say where and which way;
239
+ `residual_rms_db` and `residual_within_tolerance` (against `residual_tolerance_db`) weight
240
+ each frequency by the boost asked for there, as `confidence` does. The raw
227
241
  recovered and shaping fractions are kept in the run record, not the response. A decline's `decline_message` gives the reason first, then `| found: …` with
228
242
  the same diagnosis, then the build in brackets. `beqforge/designer.py` is the
229
243
  pure `design(request) -> response` adapter, independently testable without a socket;
230
244
  `tools/designer_server.py` is the HTTP transport around it.
231
245
 
246
+ **Which answer a build gives (contract 1.4).** Every response, design or decline, and
247
+ `GET /health` carry `design_version`, `design_settings` and `designer_build`, whatever
248
+ `contract_version` the request names. `design_version` is an integer bumped only by a change
249
+ that can alter an answer for an unchanged request: the candidates, the rejected designs, a
250
+ decline or a fit figure. Wording, speed and
251
+ packaging don't bump it, so the executable and the image of one release report the same
252
+ version and different `designer_build`s (the string also in `beqforge_revision`). Releases
253
+ before it: 0.0.x and 0.1.0 are 0, 0.2.0 is 1, 0.2.1 is 2, 0.2.2 is 3, and 0.2.3 to 0.2.5
254
+ are 4, as is 0.2.7, the first release to report it. One version is one algorithm, not one
255
+ filter on every machine: the `counterfactual` and `parametric` fits can land on a different
256
+ filter on a different processor, so compare versions, never figures. `design_settings` is a 12-hex digest of the server-wide settings that can change an
257
+ answer (`--strategy`, `--exclude`, `--goal-tolerance`, `--goal-tilt`, `--content-edge`,
258
+ `--device-rate`, `--coefficient-bits`, `--integer-bits`) that differ from their defaults, so a
259
+ server started with none of them always sends the same digest. `/health` also reports those
260
+ settings as `design_settings_detail`, for reading when two designers disagree. A caller keys
261
+ each title on (`design_version`, `design_settings`) to tell which titles a new release would
262
+ answer differently.
263
+
232
264
  ### Running the designer in a container
233
265
 
234
266
  `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
235
267
  pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
236
- published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
268
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
237
269
  each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
270
+ The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
271
+ as it does for the executables and PyPI; pushes to `main` and pull requests build and
272
+ smoke-test the image without publishing it.
238
273
  A manual run of that workflow with `publish` set publishes the current version's image from a
239
274
  later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
240
275
  image was published this way, the recipe having arrived after the release).
@@ -278,8 +313,10 @@ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
278
313
  ```
279
314
 
280
315
  The launcher sets the image's defaults through the environment variables above, each only if
281
- not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
282
- `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
316
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420`,
317
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache` and
318
+ `BEQFORGE_RECORD_DIR=%LOCALAPPDATA%\beqforge\records`, so every request's run record is kept.
319
+ Arguments pass through to `serve-designer`.
283
320
 
284
321
  * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
285
322
  reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
@@ -291,7 +328,8 @@ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
291
328
  `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
292
329
  Check `http://<address>:8420/health` from there first.
293
330
  * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
294
- design more than one title at a time; `/health` reports it as `parallel`.
331
+ design more than one title at a time; `/health` reports it as `parallel`. To leave room for
332
+ other work, set `BEQFORGE_CORES` to the most processors the designer may use between them.
295
333
  * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
296
334
  press Ctrl+C, to stop.
297
335
 
@@ -308,7 +346,8 @@ one locally: `uv pip install pyinstaller && uv run pyinstaller beqforge.spec`.
308
346
 
309
347
  The same workflow builds and checks the Python wheel and source distribution, verifies all
310
348
  five dependency profiles, and reuses a successful branch-push run of `tests.yml` on `main`
311
- at the exact tagged commit. If matching tests are still running, it waits up to 15 minutes;
349
+ at the exact tagged commit. If matching tests are still running, or GitHub's API fails to answer, it
350
+ keeps checking for up to 15 minutes;
312
351
  without a successful result, it runs the reusable tests workflow against the release commit.
313
352
  GitHub release creation and executable builds require those tests to pass on Linux, macOS
314
353
  and Windows, including device-check tests and isolated dependency profiles. PyPI publication
@@ -30,7 +30,6 @@ are retained so records and code comments remain traceable. Completed items are
30
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
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
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
33
  | 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
34
  | 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
35
  | 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). |
@@ -43,7 +42,8 @@ are retained so records and code comments remain traceable. Completed items are
43
42
  | 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
43
  | 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
44
  | 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). |
46
- | 27 | Parameter-space optimisation for several profiles | open | beqdesigner's main window will let a filter designer optimise a filter for chosen device profiles before publishing, in freq/Q/gain terms (a BEQ is published and loaded as parameters, so no catalogue schema change), minimising the worst error across all chosen profiles at once. The search belongs in `beqoptimiser`, beside `optimise()`; beqdesigner builds the interface. See [the request](plans/parameter-space-optimisation.md). |
45
+ | 27 | Keep run records and logs by default | open | `serve-designer` keeps no run record and no log file unless told to, so the Windows designer's 432 library titles (111 declines) have no records and can be reviewed only from their responses. Default records and a rotating log to the platform's per-user data directory (as the `.cmd` already does for the cache), set and declare `/records` in the image, report `record_dir` on `/health`. See [the handoff](plans/default-record-and-log-locations.md). |
46
+ | 28 | Fit sensitivity to arithmetic | open | The same request can get a different filter on a different processor. On the synthetic BW4 24 Hz title, `counterfactual` gave 2 sections here and 3 on Linux and macOS CI; `parametric`'s boost was 15.21 dB on this AVX-512 machine, 15.78 dB with Haswell BLAS kernels and 16.49 dB on CI. `flatten` matched everywhere. It's repeatable on one machine, so last-digit differences grow into a different local optimum. Measure it on real titles first: run the baseline set under `OPENBLAS_CORETYPE=Haswell` (or on another CPU) and compare verdicts and corrected curves. Then decide whether the fit needs a deterministic tie-break or a conditioning fix, or whether only the golden's coverage does. See [the correction](plans/design-version.md#correction-after-the-v026-release-tests). |
47
47
 
48
48
  ## Review evidence
49
49
 
@@ -92,10 +92,6 @@ Evidence behind the order and the parked decisions:
92
92
  filtered/1 reached 5.6 dB over 0.28 octave. Real-title candidates stayed below 0.5 dB.
93
93
  Audit this with steep policy; it is not evidence specifically of single-bin holes or
94
94
  permission to add a blanket gate.
95
- * [`designer._to_design_candidate`](beqforge/designer.py) forwards `fit_error_db` and the
96
- run's residual band, while [`filters._fit_structure`](beqforge/filters.py) returns a
97
- target-error/device-drift objective and [`design._result`](beqforge/design.py) recomputes
98
- pure target error. The actual fit score band can widen beyond the reported one.
99
95
  * The sibling [`designer-interface.md`](../beqdesigner/design/designer-interface.md)
100
96
  §§2–4 already adopts clipping cost, bass management, ordinal confidence, non-parametric
101
97
  residuals and decline/report rules. It deliberately excludes device preference.
@@ -124,20 +120,6 @@ supplying a dict is not enough to claim it was modelled. No hardware capture is
124
120
  these software comparisons. Caller propagation requires a change in beqdesigner, outside
125
121
  this repo; both its batch job and library call omit the available argument today.
126
122
 
127
- ### Residual reporting
128
-
129
- `Candidate.fit_error_db` reaches the response's `residual_db`. For fitted candidates it is
130
- normally the larger of target error and exact-coefficient quantisation change; after pruning
131
- it can be pure target error. The contract already permits residuals for every method with an
132
- explicit target and defines them as maximum absolute target error. Recompute the reported
133
- error from the canonical published cascade against its priced target on a stated band; make
134
- the evaluation rate and coefficient treatment explicit. Carry or recompute the actual band:
135
- `_fit_all` can widen it above `params.residual_band_hz`, and parametric proposals carry their
136
- own effective design settings. Keep the optimisation objective, drift screen and escalation
137
- unchanged in a reporting fix. Changing what `residual_target_db` compares against is a
138
- separate decision change and needs the full regression workflow. A small residual says the
139
- fit matched the constructed target, not that the target is correct.
140
-
141
123
  ### Fresh validation protocol and steep-filter policy
142
124
 
143
125
  The legacy corpus is development evidence after repeated tuning, not a held-out estimate.
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.7"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -40,10 +40,10 @@ still holds: no other API, no other service, and the server is a thin transport
40
40
  | `beqforge/record.py` | The run record — a fingerprinted, gzipped JSON of everything a run produced, written every run. Ours, not beqdesigner's. |
41
41
  | `beqforge/beqd.py` | Export to a `.beq` beqdesigner project. A separate job from the record: idiomatic in their UI, allowed to be lossy. |
42
42
  | `beqforge/cli.py` | `beqforge <subcommand> ...` — the installed entry point. Strips the subcommand off argv and hands the rest to the matching script in `tools/` unchanged. |
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
- | `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. |
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), `fit` (how closely the filter as played delivers the boost the content supports — a match to the target, not a verdict on it) and `alternatives` (why each other candidate lost). Reads the `Report`; measures and decides nothing. |
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; 1.3: `residual_db` and its companions come from `Candidate.target_fit`, never `fit_error_db`; 1.4: `design_version` (`DESIGN_VERSION`, bumped only by a change that can alter an answer, guarded by `tests/test_design_golden.py`), `design_settings` (`design_settings_digest`, the non-default server settings that can change an answer) and `designer_build` on every response), 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. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in a 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. A worker designs one title at a time; `--workers N` (1-8) runs N of them, each request taking the next idle one, and `/health` reports N as `parallel` so a caller can size its sends from the designer. With N > 1 each worker's fit pool is `filters.FIT_WORKERS // N` (`fit_share`) unless `BEQ_FIT_WORKERS` is set: pool size changes timing, never an answer. A worker that dies answers 500 and is replaced; the others carry on. 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`, `shared_root` and `parallel`. |
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 a 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. A worker designs one title at a time; `--workers N` (1-8) runs N of them, each request taking the next idle one, and `/health` reports N as `parallel` so a caller can size its sends from the designer. With N > 1 each worker's fit pool is the logical processors less one, divided by N and capped at `filters.FIT_WORKERS` (`fit_share`), unless `BEQ_FIT_WORKERS` is set; `--cores N` instead caps the designs' total at N, split evenly, still at most `filters.FIT_WORKERS` each: concurrent designs are what can use SMT threads, and pool size changes timing, never an answer. A worker that dies answers 500 and is replaced; the others carry on. 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`, `shared_root`, `parallel`, and 1.4's `design_version`, `design_settings`, `design_settings_detail` and `designer_build`. |
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. |
@@ -203,6 +203,16 @@ open questions it resolves. An item with no recorded outcome is not done.
203
203
 
204
204
  ## Gotchas
205
205
 
206
+ * A change that can alter an answer for an unchanged request (candidates, rejected designs, a
207
+ decline or a 1.3 fit figure) bumps `designer.DESIGN_VERSION` and regenerates
208
+ `tests/fixtures/design_golden.json` (`BEQFORGE_REGENERATE_GOLDEN=1 uv run pytest
209
+ tests/test_design_golden.py`) in the same commit. Callers decide what to redesign from it.
210
+ Wording, speed, packaging and new outputs don't bump it. If the probe shows a moved verdict
211
+ or target, the golden will usually fail too, but it covers only a few synthetic requests:
212
+ a bump follows from what the change can do, not from whether the golden noticed. It leaves
213
+ out `counterfactual` and `parametric` because their fits differ across processors
214
+ (TODO, "Fit sensitivity to arithmetic"): regenerating it on one machine doesn't make those
215
+ hold on CI.
206
216
  * RBJ biquad formulae exist twice — `biquad.py`'s class hierarchy and `filters.py`'s
207
217
  `_sos_from_parameters`. Fix both or neither; the test asserting they agree is what keeps that
208
218
  honest.
@@ -425,18 +435,19 @@ least `min_judge_octaves` above the floor — no fixed top (historical review T6
425
435
 
426
436
  ### Publication, playback and verification
427
437
 
428
- * **Robustness is measured five ways, and they answer different questions.** Do not add one
438
+ * **Robustness is measured six ways, and they answer different questions.** Do not add one
429
439
  to another, or compare one against a limit meant for another:
430
440
 
431
441
  | Quantity | What it measures | Where, over what |
432
442
  | --- | --- | --- |
433
443
  | Fit objective (`_fit_structure`'s cost) | the larger of the worst error against the target and the worst change exact-coefficient device quantisation makes to the response | the optimiser's raw parameters, over the fit's scoring band (`residual_band_hz`, widened to cover placements) |
434
- | `fit_error_db` / designer `residual_db` | that objective — **except** that when `_prune` drops a section it becomes the pure worst error against the target, without the quantisation term. Two meanings in one field (TODO 5); the escalation's `residual_target_db` compares against it | as above |
444
+ | `fit_error_db` | that objective — **except** that when `_prune` drops a section it becomes the pure worst error against the target, without the quantisation term. Two meanings in one field; the escalation's `residual_target_db` compares against it, so it is left alone. Record-only outside the fitter, never in the response | as above |
445
+ | `TargetFit` / designer `residual_db` and its 1.3 companions | the cascade that plays against the target it was fitted to: worst absolute error, where and which way, RMS and share within `fit_tolerance_db` both weighted by the boost the target asks for. Reported, never gated; a match to the target, not evidence the target is right | published parameters, quantised coefficients at the device rate, design grid, over the band the fit was actually scored on (`_fit_band_hz`, or `residual_band_hz` for `parametric`) |
435
446
  | Drift, p90 (`_published_drift`, `Verdict.drift_db`) | the 90th percentile, over `drift_samples` jitters of the *published* parameters by their rounding step, of the worst change quantisation makes — how fragile the neighbourhood is, not what the published filter does | published parameters (since the F1 follow-up both use them), 3-400 Hz design grid |
436
447
  | Exposure (`_exposure_db`) | the fallback's ranking when no fit reaches the residual target: the residual, plus the drift if over `max_drift_db`. A sampled heuristic, not a bound on the published response | as its two terms |
437
448
  | `device_error_db` and the corrected curve | what the device will actually do: the exact published, quantised response against the unquantised one (`device_error_db`), and that response applied to the programme (`verify`) — the only one that is not a model | published parameters, quantised coefficients, design grid / the programme |
438
449
 
439
- Strict Jury stability (`unstable_sections`) is separate from all five and is a veto: a
450
+ Strict Jury stability (`unstable_sections`) is separate from all six and is a veto: a
440
451
  cascade that is unstable at its published, quantised coefficients is never accepted, and its
441
452
  drift and exposure are infinite.
442
453