beqforge 0.3.3__tar.gz → 0.3.4__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 (236) hide show
  1. {beqforge-0.3.3 → beqforge-0.3.4}/.github/workflows/build-device-check.yml +1 -1
  2. {beqforge-0.3.3 → beqforge-0.3.4}/.github/workflows/build-executable.yml +2 -2
  3. {beqforge-0.3.3 → beqforge-0.3.4}/.github/workflows/tests.yml +3 -3
  4. {beqforge-0.3.3 → beqforge-0.3.4}/CLAUDE.md +2 -2
  5. {beqforge-0.3.3 → beqforge-0.3.4}/PKG-INFO +1 -1
  6. {beqforge-0.3.3 → beqforge-0.3.4}/TODO.md +2 -2
  7. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/__init__.py +1 -1
  8. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/AGENTS.md +9 -1
  9. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/accept.py +7 -5
  10. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/charts.py +7 -7
  11. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/design.py +3 -3
  12. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/designer.py +10 -8
  13. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/explain.py +4 -2
  14. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/extraction.py +88 -12
  15. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/filters.py +1 -1
  16. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/harness.py +4 -4
  17. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/impact.py +3 -0
  18. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/impact.toml +68 -0
  19. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/pipeline.py +266 -64
  20. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/record.py +12 -4
  21. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/verify.py +150 -27
  22. {beqforge-0.3.3 → beqforge-0.3.4}/negative_corpus.json +66 -66
  23. {beqforge-0.3.3 → beqforge-0.3.4}/plans/design-version.md +2 -0
  24. beqforge-0.3.4/plans/designer-latency.md +175 -0
  25. {beqforge-0.3.3 → beqforge-0.3.4}/plans/done-baseline-evidence.md +13 -0
  26. {beqforge-0.3.3 → beqforge-0.3.4}/plans/library-review-improvements.md +61 -0
  27. {beqforge-0.3.3 → beqforge-0.3.4}/plans/test-portability-and-runtime.md +14 -0
  28. {beqforge-0.3.3 → beqforge-0.3.4}/tests/fixtures/design_golden.json +1 -1
  29. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_accept.py +4 -5
  30. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_cache.py +2 -1
  31. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_channel_evidence.py +2 -2
  32. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_corpus.py +32 -0
  33. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_design.py +4 -1
  34. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_designer.py +24 -19
  35. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_device_rate.py +16 -5
  36. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_evidence.py +1 -1
  37. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_evidence_excess.py +1 -2
  38. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_exclusions.py +11 -5
  39. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_extraction.py +67 -0
  40. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_filters.py +10 -10
  41. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_guard.py +1 -3
  42. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_jacobian.py +3 -3
  43. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_pipeline.py +175 -1
  44. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_playback.py +160 -5
  45. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_playback_headroom.py +118 -4
  46. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_publication.py +6 -7
  47. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_record.py +6 -2
  48. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_reference.py +1 -1
  49. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_references.py +132 -17
  50. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_strategy_config.py +7 -8
  51. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_target_fit.py +1 -2
  52. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_timings.py +25 -1
  53. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_extract_layout.py +13 -14
  54. {beqforge-0.3.3 → beqforge-0.3.4}/tools/design_beq.py +6 -6
  55. {beqforge-0.3.3 → beqforge-0.3.4}/tools/designer_server.py +11 -11
  56. {beqforge-0.3.3 → beqforge-0.3.4}/tools/e10_cases.py +2 -2
  57. {beqforge-0.3.3 → beqforge-0.3.4}/tools/e10_run.py +1 -1
  58. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/a1_sweep.py +3 -3
  59. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/compare_verdicts.py +0 -0
  60. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/inject_variants.py +4 -4
  61. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p14_compare.py +11 -9
  62. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p14_run_pipeline.py +6 -5
  63. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_compare_structure.py +3 -1
  64. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_greedy.py +1 -1
  65. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_seeded.py +1 -2
  66. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p20_smooth_drift.py +23 -9
  67. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/probe.py +9 -6
  68. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/refit_probe.py +204 -52
  69. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/request_timing.py +3 -3
  70. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/score_injected.py +1 -1
  71. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/t8_floor_rules.py +0 -0
  72. {beqforge-0.3.3 → beqforge-0.3.4}/tools/extract.py +4 -2
  73. {beqforge-0.3.3 → beqforge-0.3.4}/tools/impact.py +1 -1
  74. {beqforge-0.3.3 → beqforge-0.3.4}/tools/negative_corpus.py +4 -4
  75. {beqforge-0.3.3 → beqforge-0.3.4}/tools/render_ledger.py +6 -3
  76. {beqforge-0.3.3 → beqforge-0.3.4}/tools/replay.py +2 -2
  77. {beqforge-0.3.3 → beqforge-0.3.4}/tools/smoke_test_exe.py +6 -6
  78. {beqforge-0.3.3 → beqforge-0.3.4}/tools/summarise.py +4 -5
  79. {beqforge-0.3.3 → beqforge-0.3.4}/tools/validate_evidence.py +6 -4
  80. {beqforge-0.3.3 → beqforge-0.3.4}/uv.lock +38 -38
  81. {beqforge-0.3.3 → beqforge-0.3.4}/.dockerignore +0 -0
  82. {beqforge-0.3.3 → beqforge-0.3.4}/.gitattributes +0 -0
  83. {beqforge-0.3.3 → beqforge-0.3.4}/.github/dependabot.yml +0 -0
  84. {beqforge-0.3.3 → beqforge-0.3.4}/.github/workflows/build-designer-image.yml +0 -0
  85. {beqforge-0.3.3 → beqforge-0.3.4}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  86. {beqforge-0.3.3 → beqforge-0.3.4}/.gitignore +0 -0
  87. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/.gitignore +0 -0
  88. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/beqforge.iml +0 -0
  89. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/editor.xml +0 -0
  90. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/jsonSchemas.xml +0 -0
  91. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/misc.xml +0 -0
  92. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/modules.xml +0 -0
  93. {beqforge-0.3.3 → beqforge-0.3.4}/.idea/vcs.xml +0 -0
  94. {beqforge-0.3.3 → beqforge-0.3.4}/AGENTS.md +0 -0
  95. {beqforge-0.3.3 → beqforge-0.3.4}/LICENCE.md +0 -0
  96. {beqforge-0.3.3 → beqforge-0.3.4}/README.md +0 -0
  97. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/AGENTS.md +0 -0
  98. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/biquad.py +0 -0
  99. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/provenance.py +0 -0
  100. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/publication.py +0 -0
  101. {beqforge-0.3.3 → beqforge-0.3.4}/beq_common/types.py +0 -0
  102. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/__init__.py +0 -0
  103. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/adjustments.py +0 -0
  104. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/beqd.py +0 -0
  105. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/biquad.py +0 -0
  106. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/cache.py +0 -0
  107. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/cli.py +0 -0
  108. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/dc_offset.py +0 -0
  109. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/diagnose.py +0 -0
  110. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/e10.py +0 -0
  111. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/fit_kernel.py +0 -0
  112. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/identify.py +0 -0
  113. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/material.py +0 -0
  114. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/reference.py +0 -0
  115. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge/rolloff.py +0 -0
  116. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge-device-check.spec +0 -0
  117. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge.spec +0 -0
  118. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/AGENTS.md +0 -0
  119. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/__init__.py +0 -0
  120. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/analyse.py +0 -0
  121. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/audio.py +0 -0
  122. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/catalogue.py +0 -0
  123. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/cli.py +0 -0
  124. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/coefficients.py +0 -0
  125. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/engines.py +0 -0
  126. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/evidence.py +0 -0
  127. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/manifest.py +0 -0
  128. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/measurement.py +0 -0
  129. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/profiles.py +0 -0
  130. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/qualification.py +0 -0
  131. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/report.py +0 -0
  132. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/transactions.py +0 -0
  133. {beqforge-0.3.3 → beqforge-0.3.4}/beqforge_device_check/verify.py +0 -0
  134. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/AGENTS.md +0 -0
  135. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/__init__.py +0 -0
  136. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/cache.py +0 -0
  137. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/cli.py +0 -0
  138. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/core.py +0 -0
  139. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/data/seed-manifest.json +0 -0
  140. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/data/seed.json.gz +0 -0
  141. {beqforge-0.3.3 → beqforge-0.3.4}/beqoptimiser/parameters.py +0 -0
  142. {beqforge-0.3.3 → beqforge-0.3.4}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  143. {beqforge-0.3.3 → beqforge-0.3.4}/docs/device-check-examples/simulation.json +0 -0
  144. {beqforge-0.3.3 → beqforge-0.3.4}/docs/device-check.md +0 -0
  145. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/README.md +0 -0
  146. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  147. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  148. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/assets/example-1.png +0 -0
  149. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/assets/example-2.png +0 -0
  150. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/entry-results.json.gz +0 -0
  151. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser-report/statistics.json +0 -0
  152. {beqforge-0.3.3 → beqforge-0.3.4}/docs/optimiser.md +0 -0
  153. {beqforge-0.3.3 → beqforge-0.3.4}/evidence_validation.json +0 -0
  154. {beqforge-0.3.3 → beqforge-0.3.4}/packaging/designer/Dockerfile +0 -0
  155. {beqforge-0.3.3 → beqforge-0.3.4}/packaging/designer/beqforge-designer.cmd +0 -0
  156. {beqforge-0.3.3 → beqforge-0.3.4}/packaging/designer/smoke.py +0 -0
  157. {beqforge-0.3.3 → beqforge-0.3.4}/packaging/device-check/NOTICE.txt +0 -0
  158. {beqforge-0.3.3 → beqforge-0.3.4}/plans/E10-validation-protocol.md +0 -0
  159. {beqforge-0.3.3 → beqforge-0.3.4}/plans/F2-catalogue-filter-optimisation.md +0 -0
  160. {beqforge-0.3.3 → beqforge-0.3.4}/plans/F2-device-precision-validation.md +0 -0
  161. {beqforge-0.3.3 → beqforge-0.3.4}/plans/F2-package-consolidation.md +0 -0
  162. {beqforge-0.3.3 → beqforge-0.3.4}/plans/R2a-server-stage-cache.md +0 -0
  163. {beqforge-0.3.3 → beqforge-0.3.4}/plans/bass-ceiling.md +0 -0
  164. {beqforge-0.3.3 → beqforge-0.3.4}/plans/contract-1.5-adjustments.md +0 -0
  165. {beqforge-0.3.3 → beqforge-0.3.4}/plans/default-record-and-log-locations.md +0 -0
  166. {beqforge-0.3.3 → beqforge-0.3.4}/plans/done-design-and-pipeline.md +0 -0
  167. {beqforge-0.3.3 → beqforge-0.3.4}/plans/e10-titles-v1.json +0 -0
  168. {beqforge-0.3.3 → beqforge-0.3.4}/plans/fit-kernel.md +0 -0
  169. {beqforge-0.3.3 → beqforge-0.3.4}/plans/fit-weighting.md +0 -0
  170. {beqforge-0.3.3 → beqforge-0.3.4}/plans/library-v5-review.md +0 -0
  171. {beqforge-0.3.3 → beqforge-0.3.4}/plans/parameter-space-optimisation.md +0 -0
  172. {beqforge-0.3.3 → beqforge-0.3.4}/plans/playback-contract.md +0 -0
  173. {beqforge-0.3.3 → beqforge-0.3.4}/plans/redesign-scope.md +0 -0
  174. {beqforge-0.3.3 → beqforge-0.3.4}/plans/research-design-decisions.md +0 -0
  175. {beqforge-0.3.3 → beqforge-0.3.4}/plans/texture-yardstick-sub-band.md +0 -0
  176. {beqforge-0.3.3 → beqforge-0.3.4}/plans/texture-yardstick-sub-band.patch +0 -0
  177. {beqforge-0.3.3 → beqforge-0.3.4}/pyproject.toml +0 -0
  178. {beqforge-0.3.3 → beqforge-0.3.4}/tests/__init__.py +0 -0
  179. {beqforge-0.3.3 → beqforge-0.3.4}/tests/conftest.py +0 -0
  180. {beqforge-0.3.3 → beqforge-0.3.4}/tests/fixtures/optimiser_seed_entry.json +0 -0
  181. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_cli.py +0 -0
  182. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_adjustments.py +0 -0
  183. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_cache_stores.py +0 -0
  184. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_charts.py +0 -0
  185. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_dc_offset.py +0 -0
  186. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_designer_server.py +0 -0
  187. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_diagnose.py +0 -0
  188. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_explain.py +0 -0
  189. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_frozen_cache.py +0 -0
  190. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_golden.py +0 -0
  191. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_harness.py +0 -0
  192. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_headroom.py +0 -0
  193. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_hum.py +0 -0
  194. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_identify.py +0 -0
  195. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_impact.py +0 -0
  196. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_parallel.py +0 -0
  197. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_record_fingerprint.py +0 -0
  198. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_ripple_band.py +0 -0
  199. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_rolloff.py +0 -0
  200. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_design_verify.py +0 -0
  201. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_designer_image.py +0 -0
  202. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_designer_server_startup.py +0 -0
  203. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_analyse.py +0 -0
  204. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_catalogue.py +0 -0
  205. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_catalogue_predict.py +0 -0
  206. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_cli.py +0 -0
  207. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_engines.py +0 -0
  208. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_evidence.py +0 -0
  209. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_examples.py +0 -0
  210. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_manifest.py +0 -0
  211. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_measurement.py +0 -0
  212. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_qualification.py +0 -0
  213. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_transactions.py +0 -0
  214. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_device_check_verify.py +0 -0
  215. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_e10.py +0 -0
  216. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_fit_kernel.py +0 -0
  217. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_optimiser.py +0 -0
  218. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_optimiser_cache.py +0 -0
  219. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_optimiser_parameters.py +0 -0
  220. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_optimiser_report.py +0 -0
  221. {beqforge-0.3.3 → beqforge-0.3.4}/tests/test_package_boundaries.py +0 -0
  222. {beqforge-0.3.3 → beqforge-0.3.4}/tools/__init__.py +0 -0
  223. {beqforge-0.3.3 → beqforge-0.3.4}/tools/build_optimiser_seed.py +0 -0
  224. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/compare_records.py +0 -0
  225. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p14_jacobian.py +0 -0
  226. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p14_surrogate.py +1 -1
  227. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_compare_greedy.py +2 -2
  228. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_compare_seeded.py +2 -2
  229. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/p18_structure.py +1 -1
  230. {beqforge-0.3.3 → beqforge-0.3.4}/tools/experiments/parameter_benchmark.py +0 -0
  231. {beqforge-0.3.3 → beqforge-0.3.4}/tools/ledger_template.html +0 -0
  232. {beqforge-0.3.3 → beqforge-0.3.4}/tools/optimiser_catalogue_report.py +0 -0
  233. {beqforge-0.3.3 → beqforge-0.3.4}/tools/prepare_device_check_helper.py +0 -0
  234. {beqforge-0.3.3 → beqforge-0.3.4}/tools/seed_optimiser_cache.py +0 -0
  235. {beqforge-0.3.3 → beqforge-0.3.4}/tools/smoke_test_device_check.py +0 -0
  236. {beqforge-0.3.3 → beqforge-0.3.4}/tools/smoke_test_install.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.3.2
64
+ - uses: astral-sh/setup-uv@v10.3.3
65
65
  with:
66
66
  enable-cache: true
67
67
  - name: Install Linux audio and USB libraries
@@ -112,7 +112,7 @@ jobs:
112
112
  contents: read
113
113
  steps:
114
114
  - uses: actions/checkout@v7
115
- - uses: astral-sh/setup-uv@v10.3.2
115
+ - uses: astral-sh/setup-uv@v10.3.3
116
116
  with:
117
117
  enable-cache: true
118
118
  - name: Build wheel and source distribution
@@ -201,7 +201,7 @@ jobs:
201
201
  - uses: actions/checkout@v7
202
202
 
203
203
  - name: Install uv
204
- uses: astral-sh/setup-uv@v10.3.2
204
+ uses: astral-sh/setup-uv@v10.3.3
205
205
  with:
206
206
  enable-cache: true
207
207
 
@@ -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.3.2
70
+ uses: astral-sh/setup-uv@v10.3.3
71
71
  with:
72
72
  enable-cache: true
73
73
 
@@ -100,7 +100,7 @@ jobs:
100
100
  - uses: actions/checkout@v7
101
101
 
102
102
  - name: Install uv
103
- uses: astral-sh/setup-uv@v10.3.2
103
+ uses: astral-sh/setup-uv@v10.3.3
104
104
  with:
105
105
  enable-cache: true
106
106
 
@@ -131,7 +131,7 @@ jobs:
131
131
  profile: [base, designer, optimiser, device-check, all]
132
132
  steps:
133
133
  - uses: actions/checkout@v7
134
- - uses: astral-sh/setup-uv@v10.3.2
134
+ - uses: astral-sh/setup-uv@v10.3.3
135
135
  with:
136
136
  enable-cache: true
137
137
  - name: Build the single distribution
@@ -12,8 +12,8 @@
12
12
  protocol for anything that can change a decision, and the outcome written into
13
13
  `TODO.md` and the completed evidence under `plans/`. Don't substitute a full nine-title rerun, and don't skip it.
14
14
  * Don't run `uv run python tools/design_beq.py` casually against real material — a run is
15
- 40-80 s a title. Wrap any timed run in `systemd-inhibit` (see AGENTS.md's "Waiting on a long
16
- run") and use the background-task workflow rather than a foreground `sleep` loop.
15
+ 40-80 s a title. Wrap any timed run in `systemd-inhibit` (see AGENTS.md's "Long-running jobs and
16
+ timing") and use the background-task workflow rather than a foreground `sleep` loop.
17
17
  * The stage cache (`beqforge/cache.py`) is on by default and keyed per stage; `--fresh` forces a
18
18
  recompute, `--no-cache` uses neither. Don't assume a change took effect without one of these if
19
19
  you're re-running against the same material.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -47,14 +47,14 @@ ID alone (AGENTS.md says where each lives), and the stale contract versions are
47
47
  | 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. |
48
48
  | 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. |
49
49
  | 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. |
50
- | 23 | Performance | fit kernel released in 0.3.0 (design version 8); 0.3.1 fixes the designer image run as another user | [The measurement and outcome](plans/fit-kernel.md#outcome). The fit's search now scores each DE generation at once in the linear domain (`beqforge/fit_kernel.py`; numba, numpy fallback, bit-identical). Real runs on the 11 titles: 703 to 283 s wall, 1905 to 373 CPU-s; the `fit` stage 10x, so a title is 28 s and 37 CPU-s. A re-roll: refit probe no worse on balance, no baseline title's winner or verdict changed in real runs, protocol and corpus unchanged where gated. **Left:** the fit re-rolls near the top of the band as freely as at 10 Hz (row 37: weighting the search works, held back behind row 36); measure a library sample for the impact basis and replace it in a later 0.3 release (0.3.0's is mostly injected variants); CI's Windows and macOS executables with numba. Still parked: reuse of repeated numerical identities, the ~9 s a title outside the pipeline (measure on the server path), an own DE loop. GPU offload rejected. Missed-fit search belongs to F3/F4. |
50
+ | 23 | Performance | fit kernel released in 0.3.0 (design version 8); 0.3.1 fixes the designer image run as another user | [The measurement and outcome](plans/fit-kernel.md#outcome). The fit's search now scores each DE generation at once in the linear domain (`beqforge/fit_kernel.py`; numba, numpy fallback, bit-identical). Real runs on the 11 titles: 703 to 283 s wall, 1905 to 373 CPU-s; the `fit` stage 10x, so a title is 28 s and 37 CPU-s. A re-roll: refit probe no worse on balance, no baseline title's winner or verdict changed in real runs, protocol and corpus unchanged where gated. **Left:** the fit re-rolls near the top of the band as freely as at 10 Hz (row 37: weighting the search works, held back behind row 36); measure a library sample for the impact basis and replace it in a later 0.3 release (0.3.0's is mostly injected variants); CI's Windows and macOS executables with numba. **Repeated work removed** (not yet released; no design version bump, no answer changed): seven exact-preserving changes, one commit each, each with probe `--tol 0` unchanged and real-run records identical — the headroom peak in one-second blocks, the sub feed and its unfiltered spectra built once a run, one filtered waveform per measured candidate, a counterfactual cap's restoration reused when its boosts repeat, one flat deficit a channel, and the bootstrap's percentile read off draw counts (bit-identical; the review's prototype differed by 2e-15 dB only through the layout `std` read). Caught Stealing 54.7 to 27.5 s cold and 28.9 to 15.2 s warm in the pipeline, Black Bag 22.1 to 13.9 and 12.3 to 8.1; see [the outcome](plans/designer-latency.md). Follow-up (the mix's flat deficit measured twice a run not five times, the record's chart transfer built once a candidate: Caught Stealing warm 22.3 to 20.6 s wall, Black Bag 14.2 to 12.3, records identical). Still parked: the ~7 s a title outside the pipeline that remains (npz decompression 1.5 s, imports 0.8 s, record 0.6 s, chart curves 4 s; measure on the server path), an own DE loop. GPU offload rejected. Missed-fit search belongs to F3/F4. |
51
51
  | 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. |
52
52
  | 26 | Texture yardstick retune (library review) | blocked on E10 | 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). The reopening trigger (the library review) is met and the first measurement is done ([research item 4](plans/library-review-improvements.md#4-the-texture-retune-row-26)): the trend degree does nothing; ending the band at 80 Hz clears five of the seven review declines but lets five more gated negatives past this rule and costs three filtered cases their passband. Its full corpus run lets in exactly `broadband/5` (a false acceptance at a 2.5 dB boost; gate 0/45 to 1/45, upper bound 0.118, FAIL) and moves nothing else, `filtered` included ([the run](plans/library-review-improvements.md#4-the-texture-retune-row-26)). So the open question is the boost-weighted gate, and E10 (row 3) before adopting any retune. 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). |
53
53
  | 28 | Fit sensitivity to arithmetic | released in 0.2.10 (design version 6) | Identification now converges to 1e-12 and the fit gets its target and ceiling quantised to 0.01 dB. The refit probe gives the same filters natively and as an emulated older CPU (0 of 11 titles moved; 28 Years Later's parametric moved before). Protocol false acceptances unchanged; corpus gate pass, `filtered` 7/9 (was 6/9). The 11 titles' corrected curves stay within 1.3 dB, though three swap between near-tied winners ([outcome](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6)). CI's verdict on the golden: `counterfactual` matches on Linux, Windows and macOS and is back in it; `parametric` matches on the x86 runners that disagreed in 0.2.6, but macOS arm64 publishes the same filters with figures 0.06 dB apart, so it stays out ([verdict](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6)). Left: find where arm64's parametric fit parts from x86's (needs an arm64 machine: compare the identified target, the quantised fit input and the fitted cascade unrounded) and either close the gap or accept it and say so in the golden's docstring; until then an Apple-silicon caller can see parametric figures a few hundredths of a dB from an x86 one. From design version 8 (row 23's kernel) `counterfactual` on the golden's rolloff title also publishes one step away on arm64 (36.25 Hz against 36.24, figures within tolerance), so the golden skips that case on arm64 too. Ship before the version 5 redesign (row 31) so the library is redesigned once. |
54
54
  | 29 | Library review improvements | fixes released; research recorded | From beqdesigner's review of all 279 declines in its full-library run ([the proposal](plans/library-review-improvements.md), with evidence, ids and run records per item). Released in 0.2.9 as design version 5 ([outcomes](plans/library-review-improvements.md#outcomes)): fix 1 (the unmeasured floor wording), fix 2 (the fit scored on the published parameters; The Quiet Girl now designs, Bugonia now declines on coverage below 8.1 Hz), fix 3 (mains-hum notches filled), fix 4 (the fit bounded by the over-lift bar; Crazy Heart's flatten passes), O2 (an infrasonic plateau refused as the reference) and O1 (a stepping DC offset reported). Research, measured and not built ([research](plans/library-review-improvements.md#research-after-the-fixes)), now tracked elsewhere: 1b (textured passband) has no contained fix and is a plateau recalibration for E10 (row 3); 4 is row 26; 5 (per-title scene margin, which also covers item 7) is an experiment for the protocol, corpus and E10; 6 (a sub-band reference) rescues one plausible title and is not worth building on this evidence; item 8's machine dependence is row 28. Not followed up: item 3's tilt band. Fix 2 rescued The Quiet Girl, and The Smurfs and Despicable Me 3 already design here, but Blood Simple and Synecdoche, New York haven't been rerun; check them in row 31. |
55
55
  | 30 | Contract 1.5: design adjustments | released in 0.2.9; one gap parked | beqdesigner can ask for a different design per title: ignore a band, a floor, a boost cap, a tilted goal, a note. `beqforge/adjustments.py` answers each; default answers unchanged (golden; probe `--tol 0`, all 11 titles). See [the outcome](plans/contract-1.5-adjustments.md#outcome). Only gap: a note's text is not written into the run record (the record holds the adjusted params, so the design replays). Reopen if a review needs the note from the record. |
56
56
  | 31 | Review the version 5 library redesign | open: handoff received 23 October 2026; group 1 released in 0.2.10 (design version 6) | Outcome and evidence: [the v5 review](plans/library-v5-review.md). beqdesigner's v5 rerun (job `ef4265fa`, 1,083 titles, about 1,000 designed and 69 declined; 18 titles v4 designed now decline, about half wrongly) is triaged into four groups in `/media/beq/triage/handoff-v5.md` (queue ids, v5 records and earlier runs per title). 1 (built): O2 refused a plateau starting just under 10 Hz on the knee of a real rolloff (A Quiet Place: Day One, Elysium, Pan); it now keeps such a plateau when it reaches 20 Hz and the shortfall beneath exceeds the passband's ripple, and all three design. Probe, protocol and corpus unchanged. 2: a whole candidate rejected for missing one check by a fraction: cliff by under 3 dB/oct (Heartburn, Back to the Future, Guardians of the Galaxy, The Commitments), tilt (Guardians, The Exorcist, Alien Resurrection, Philadelphia), level (Ray), F9 residual just below the content edge (Drive, Amy). The library has no negatives, so a tolerance it suggests still goes through the protocol and E10, never the catalogue. 3: every candidate corrected only down to above the content edge (Return to Seoul, Spirited Away, The Jungle Book, Bugonia). 4: known, rows 5/6 (steep filter; `judge_from_content_edge` is a server flag, not on the wire), F5 (Eastern Promises, Fame, Home Alone) and row 26. Earlier scope of this row: 0.2.9 reports design version 5, so beqdesigner's out-of-date filter flags every title designed before it. Review the redesign's declines and changed designs from their run records, as the first review was (row 29): confirm the fixes' titles moved as measured, rerun the open cases (row 29's tilt titles, the near-texture watch cases Black Bag, Bugonia and Incredible Hulk), and collect real-title evidence for rows 26 and 29's research. It can show wrong declines but not false acceptances (the library has no known negatives), so a retune it suggests still goes through E10. Do row 28 first if both machines will design. Records are kept by default from 0.2.10 (row 27, done). |
57
- | 32 | Fit optimum selection | part built (design version 7, not released); strategy choice still unstable | Found by row 28: reproducible is not stable. Changing the fit's input by at most 0.005 dB changed 24 winners on 54 titles and variants; Black Bag LR4 35's recovery fell from a 13.1 dB to a 3.4 dB peak. **Built (25 October 2026):** the fitter returns every fit within its 0.5 dB residual bar of its answer, and the pipeline keeps the first the judge passes, fewest sections first; an alternative must lift by more than the goal tolerance to replace the answer. Perturbing the fit input by 0.004 dB now flips 14 winners where HEAD flipped 19, 33 verdicts against 36, and no title swaps between designed and declined (HEAD 1). Black Bag LR4 35 goes to a 0.4 dB shortfall and holds; 13 of row 35's 14 new declines design again and none of its 17 gains is lost. Protocol unchanged, corpus gate passes. **Left:** most remaining flips are which strategy wins (flatten against a counterfactual on the steep variants, up to 38 dB apart below the content edge). The choice among strategies is untouched, and that is the next research step. See [the outcome](plans/library-review-improvements.md#row-32-choosing-among-near-equal-fits-25-october-2026-design_version-7) and [the finding](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6). |
57
+ | 32 | Fit optimum selection | part built (design version 10, not released); ties now broken by strategy order; verdict flips left | Found by row 28: reproducible is not stable. Changing the fit's input by a few thousandths of a dB changes winners. **Built (25 October 2026, version 7):** the fitter returns every fit within its 0.5 dB residual bar of its answer, and the pipeline keeps the first the judge passes, fewest sections first; an alternative must lift by more than the goal tolerance to replace the answer ([outcome](plans/library-review-improvements.md#row-32-choosing-among-near-equal-fits-25-october-2026-design_version-7)). **Measured again (2 November 2026):** `refit_probe.py`'s perturbation stacked per worker and read cached proposals made under another offset, so every earlier flip count was indicative only; fixed, and `rejudge` now judges again from kept fits in seconds. On 32 titles (the 11 baseline titles and the 21 variants that had ever moved) at three offsets, version 9 flipped 14 of 93 winners (28 with `--content-edge`), and real titles flip too: 28 Years Later and Send Help move between parametric and flatten, under 0.1 dB apart, because the tie went to the fewest sections and a near-equal fit's section count re-rolls. Searching the outranked fit again from other seeds (A) and per-octave departure (C) changed little and are not adopted. **Built (version 10):** a flatness tie goes to the strategy listed first in `STRATEGIES`: 8 flips (18), every default-mode ranking flip gone. Probe: the two real titles go to flatten, the same answer within 0.6 dB; protocol seed 101 improves one recovery, 947 unchanged; corpus gate unchanged. On the v8 library 80 of 1,069 winners change (70 of 762 all passing, 10 of 271 with a failure). **Left:** verdict flips, a candidate on the edge of a shape check passing under one offset and failing under the next (8 of the 8 left, 11 of 18 with `--content-edge`). Next: a margin around those checks. See [the outcome](plans/library-review-improvements.md#a-stable-tie-break-2-november-2026-design_version-10) and [the finding](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6). |
58
58
  | 33 | Noise lift beyond the evidence (found on a pumping floor) | open: the evidence bound was measured and not adopted; next, validate the floor detection | E10's first runs lifted `nonstationary_floor` 27-33 dB, but that recipe swung about 37 dB inside every event: event-locked content, matching real stopband leakage on contrast and coherence, so not a failure under the content principle ([v5](plans/E10-validation-protocol.md#the-pumping-floor-v5-28-october-2026) reports it as `event_locked_floor` and counts realistic 2-6 dB `tracking_floor` and `breathing_floor`). On seeds 1-12 under [v6](plans/E10-validation-protocol.md#the-skirt-and-the-cases-it-leaves-v6-28-october-2026) (noise lift counted beyond half an octave under the truth band), 4 of 72 realistic-floor rows lift noise. The two large ones are the judge passing the candidate that fits worst: on `breathing_floor/2` four three-section fits within 1.1-2.1 dB of target fail the cliff check, and a single shelf 6.4 dB off target and 5.5 dB over the evidence ceiling passes and is selected; on `breathing_floor/5` (gentle) a single section 9.0 dB over the ceiling at an unsupported bin is selected over a passing three-section fit 1.9 dB over. Nothing refuses a design for exceeding its evidence: the over-lift bar is measured from the goal, and `evidence_excess` is reported only. **Bounding lift by the evidence, not adopted (29 October 2026):** the fitter bound at the licence plus 1.5 dB (A), a judge check on any excess over 1.5 dB (B), and that check limited to excesses at least half an octave wide (C) were each built on branch `experiment-evidence-bound`, uncommitted. C cleared E10's counted noise lifts, but applied to the v8 library's stored `evidence_excess` it refuses the accepted design for 96 of 1,033 titles, and 14 of those would decline. Inspection showed that most of these are a smooth fit against a ragged licence on content that tracks the programme (Any Given Sunday, To Die For). The large lifts it catches below a diagnosed floor (Wait Until Dark, White Heat) sit inside a licence that allows 20-25 dB there, and C catches them only by an edge excess. **Lift below the diagnosed floor:** 247 of the 304 designed titles with measured bins under `noise_floor_hz` lift more than 3 dB there, and 151 more than 10 dB. Almost all are flatten, holding its lift flat, among them Moana (37 dB under 8.4 Hz) and White Heat (29 dB under 40 Hz). These are not counts of noise lift. `noise_floor_hz` is the mix tracking boundary, the first band whose envelope fails to track the passband, and failing that is not proof of noise. The contrast licence often disagrees with it (Wait Until Dark: 20 dB licensed below it). **On E10's truth** ([evidence](plans/E10-validation-protocol.md#is-the-tracking-boundary-a-noise-detector-29-october-2026)), the boundary sits 0.24-0.42 octave (median) below the true noise boundary on every floor family, up to 1.12 octave, and never above it: it calls the noise just above it content. It also misses half of `finite_stopband`'s exposed cases, and calls LFE-only content noise at 15-27 Hz on 3 of 12 `partial_channels`. **On the library**, an independent test calibrated on E10 (does the band's level scale with programme loudness, and is it steady) finds content under the boundary on 151 of 300 floored titles, among them Moana, Creed III and Juror #2, the largest lifts below it. Only 10 look like steady noise. The other 139, mostly older, neither scale nor stay steady, which these statistics cannot resolve. So the boundary is not fit to gate a rule, and its content-as-noise error also holds restorations flat that the content supports. Measured within 240 s stretches, only 27 of those 139 become steady noise; 85 stay bursty within minutes without scaling with the plateau, which is either content independent of the plateau or transient noise (splices, handling), and E10 has no transient noise to calibrate against. **Next:** listen to Wait Until Dark, Marnie and GoodFellas (bursty, not scaling) against Mary Poppins and The Exorcist (steady noise). If bursty and non-scaling proves to be noise, add a transient-noise family to E10. Then improve the floor detection itself, before deciding how a restoration should behave below a floor shown to be noise: hold flat, cap at the local target as the counterfactuals do, or return to 0 dB. [Evidence](plans/E10-validation-protocol.md#bounding-lift-by-the-evidence-three-experiments-none-adopted-29-october-2026). Related: rows 32, 36, E4/E9. |
59
59
  | 34 | Redesign scope | built and released in 0.2.12 (contract 1.6's `POST /impact`); beqdesigner's step 2 live check passed, step 4 verification next | The pipeline is a catalogue of 37 named features (`beqforge/impact.py`), each owning its code and reading from a run record how a design relied on it. Each answer-changing design version declares in `beqforge/impact.toml` which features it changed and how much that moved; `tools/impact.py check` refuses changed code no feature owns and undeclared answer-reaching changes (regression step 5). `POST /impact` scores up to 500 records (15 s over the share), and records from before 0.2.11 are identified by the release that wrote them, so the current library can be scored. Version 7's fit-choice degree was measured on the library on one machine: 18 of 34 post-fit declines moved, 7 of 15 designs with a failing candidate, 2 of 15 with all passing, 0 of 8 analysis declines. Against the library: 34 likely, 1,015 possible, 236 unaffected, 70 unknown (pre-design-version builds). It doesn't meet the plan's bar for this change (likely holds 18 of the sample's 27 movers): a change that re-rolls the fit can't be narrowed by outcome class. The ranking still holds: the top 30% hold about two thirds of the expected movers. Records now carry each verdict's margins and each fit `_choose_fit` had, so a threshold change is scored exactly (`will_move` or `unaffected`), and `unknown` (`no_figure`) only for older records or a losing candidate that would start passing; this changes no answer (rejudge probe 11 of 11 at `--tol 0`, refit probe 0 of 54 moved). Responses carry each change's `basis` and `unknown_reason` (agreed with beqdesigner). `impact.MOVED`'s cascade line is now 20/f dB, capped at 2 dB at 10 Hz and below, not a flat 1 dB. Version 7's designed classes were recounted on beqdesigner's 105-title sample: 16 of 35 with a failing candidate, 22 of 60 all passing. **Version 9's scores (1 November 2026):** the ceiling's basis came from a sample chosen for lifting above 60 Hz, and its two touches were combined as independent although they were one measurement, so every one of the v8 library's 1,033 designs scored `likely`. Each touch can now be confined above a frequency (`above_hz`, `basis_below`). A record is scored by whether its band, target or cascade reaches above 60 Hz, and the largest share among a change's touches stands for the change. That gives 365 `likely` and 684 `possible` (0.29, row 32's re-roll); expected movers fall from 999 to 532. No answer changes (probe 11 of 11 at `--tol 0`). See [the outcome](plans/redesign-scope.md#scoring-a-change-by-what-it-reaches-1-november-2026). **Left:** beqdesigner's step 2 live check and step 4 verification sample (105 titles), and the check's from-the-results direction. See [the outcome](plans/redesign-scope.md#outcome). |
60
60
  | 35 | Review the version 6 library redesign | open: handoff received 24 October 2026; the 500 and record overwriting fixed in 0.2.11; the 14 new declines traced to row 28, 13 of them designing again on row 32 (version 7, unreleased) | beqdesigner's v6 rerun (job `e6a70f7e`, 1,287 titles): 1,016 designed and 270 declined. A Quiet Place: Day One, Elysium and Pan now design (row 31's O2 fix). 17 titles declined on v5 now design, and 14 designed on v5 now decline. Handoff: `/media/beq/triage/handoff-v6.md`; ledger `/media/beq/triage/ledger.jsonl`, stage `E`. **Fixed:** A Quiet Place Part II got a 500 every time, because `TargetFit.within_tolerance` came back as 1.0000000000000002. Zero-weight bins that miss the tolerance change how numpy pairs up a sum, so the in-tolerance sum could round above the full one. It is now met / (met + missed), which can't exceed 1; this code predates row 28. The figure is reporting only, so no design version: the title's filters are unchanged (low shelf 18.25 Hz +24.6 dB, peak 28.38 Hz −3.5 dB), and its response is now valid. Probe 11 of 11 unchanged at `--tol 0`; suite passes. **The 14 new declines (analysed 24 October 2026):** all 14 were rerun on one machine, at v0.2.9, at `f6dca94` (row 28 alone) and at HEAD, from requests identical to the library's (same digest). All 14 design on 0.2.9 and decline at `f6dca94`; HEAD agrees with `f6dca94`. So row 28 alone is the cause, and processor variance accounts for none of them. In every case the target is identical (0.000 dB apart) and the fit lands on a different cascade: row 28's 0.01 dB quantisation of the fit's input re-rolls the fitter's choice among near-equal optima (row 32). Except for TINA, the new fits are within 1 dB of the old by the fitter's own measure, worst-case error (Kung Fu Panda 4 0.4-0.9 dB worse, You've Got Mail 0.9 dB better), yet a judge check that depends on the error's shape flips: cliff (Platoon, The Manchurian Candidate, You've Got Mail, How to Marry a Millionaire), tilt (On Golden Pond, Christine, Twilight's counterfactual), ripple (Arrival, Kung Fu Panda 4), turnover (Magnolia, Zoolander), extent (Twilight's flatten) or slot (Rocky, Souleymane's Story). In TINA's case the fit itself is worse: 0.2.9's three-section fits drifted 2.1-3.0 dB on publication rounding, against a 3.0 limit, with their lowest sections about 4 quantisation steps from the device's DC limit. The new fits most likely drifted just past it, and the exposure rule fell back to one shelf fitting 4-6 dB off. No threshold should move on this evidence: the declines are as arbitrary as the 0.2.9 acceptances were, and the fix is row 32's, choosing among near-equal fits by what the judge checks rather than by which optimum the arithmetic lands on. **Slot check:** Rocky's and Souleymane's Story's new fits carry sections under 1 dB that the pruner keeps, because dropping them costs 1.02 and 1.12 dB of worst-case error against its 1 dB allowance. Their 0.2.9 two-section fits reached the same error without them. `designer-interface.md` §4 allows a rejection only for a reason internal to the evidence, which a redundant section is not, so the stated reason is wrong whatever the outcome. The outcome itself is doubtful too: both designs are 0.2-2.6 dB sections at 71-353 Hz, mostly above an 80 Hz crossover, not low-end restoration. The library's flip lists are not ground truth for row 34 by themselves (see [the proposal](plans/redesign-scope.md#validation-before-relying-on-it)): its v5 records were overwritten by v6's, because a record was named by its request's audio digest. **Fixed (0.2.11):** a record is now named by digests of the audio and of the rest of the request (coverage, bass management, adjustments, settings) and by the design version, and holds `design_version` and `design_settings` itself. A response's `run_record` (1.6, agreed in principle with beqdesigner) names it relative to the shared root. No answer changes. |
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.3.4"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -151,6 +151,14 @@ something that decides anything is committed. The synthetic protocol and corpus
151
151
  a decision-changing fitter change; run the two protocol seeds in parallel. `probe.py snapshot
152
152
  --content-edge --records DIR` likewise rejudges any scratch record set with the option on.
153
153
 
154
+ A change to fit selection or the judge alone does not need refitting: take "before" with
155
+ `snapshot --state <scratch>/state`, and `refit_probe.py rejudge ... --state <scratch>/state`
156
+ judges and chooses again from the kept fits in seconds (`REFIT_REJUDGE_FRESH=1` when the judge
157
+ itself changed, about seven minutes over the steep variants). In a worktree, run the tool with
158
+ the main checkout's `.venv/bin/python`: `uv run` there builds an environment without the
159
+ extras. Stability under another machine's arithmetic (TODO 32) is `REFIT_PERTURB_DB`; see the
160
+ tool's docstring.
161
+
154
162
  **3. Act on what moved:**
155
163
 
156
164
  * **Nothing** → the change reaches no decision on this material. The test suite is still
@@ -532,7 +540,7 @@ least `min_judge_octaves` above the floor — no fixed top (historical review T6
532
540
 
533
541
  * A run is ~40-80 s a title (down from ~100-490 s, 7.18x, every accepted filter's verdict
534
542
  preserved); the test suite is ~2 minutes. Run both in the background regardless — see
535
- "Waiting on a long run" above. Measured 2026-10-25 on 0.2.12 (warm cache, 10 designing
543
+ the root AGENTS.md's "Long-running jobs and timing". Measured 2026-10-25 on 0.2.12 (warm cache, 10 designing
536
544
  titles, 70 s and 190 CPU-s a title on average): `fit` is 41% of wall time and 70% of CPU,
537
545
  `parametric`'s own fit inside its target stage another ~10% and ~12%, the rest of target
538
546
  construction ~12%, judging 17% (verify, headroom, sub feed) and ~13 s a title falls outside
@@ -24,11 +24,11 @@ import numpy as np
24
24
  from beqforge import DESIGN_GRID, BiquadSpec
25
25
  from beqforge.filters import (
26
26
  Realisation,
27
- publication_filters,
28
- unstable_sections,
29
27
  biquad_sos,
30
28
  drift_distribution,
31
29
  magnitude_db,
30
+ publication_filters,
31
+ unstable_sections,
32
32
  )
33
33
  from beqforge.verify import Correction
34
34
 
@@ -603,9 +603,11 @@ def assess(
603
603
  return Verdict(
604
604
  passed=False,
605
605
  failures=[
606
- f"only {octaves:.2f} octaves left to judge "
607
- f"({correction.band_hz[0]:.1f}-{correction.band_hz[1]:.1f} Hz); a slope read "
608
- f"over less than {params.min_judge_octaves:g} is set by where the band stops"
606
+ (
607
+ f"only {octaves:.2f} octaves left to judge "
608
+ f"({correction.band_hz[0]:.1f}-{correction.band_hz[1]:.1f} Hz); a slope read "
609
+ f"over less than {params.min_judge_octaves:g} is set by where the band stops"
610
+ )
609
611
  ],
610
612
  notes=[],
611
613
  worst_gradient_before=math.nan,
@@ -34,13 +34,13 @@ import matplotlib
34
34
 
35
35
  matplotlib.use("Agg") # written to file, never shown; plt.show() blocks a headless run
36
36
 
37
- import matplotlib.pyplot as plt # noqa: E402
38
- import numpy as np # noqa: E402
37
+ import matplotlib.pyplot as plt
38
+ import numpy as np
39
39
 
40
- from beqforge import BiquadSpec # noqa: E402
41
- from beqforge.filters import Realisation, unstable_sections # noqa: E402
42
- from beqforge.verify import device_waveform # noqa: E402
43
- from beqforge.material import Material # noqa: E402
40
+ from beqforge import BiquadSpec
41
+ from beqforge.filters import Realisation, unstable_sections
42
+ from beqforge.material import Material
43
+ from beqforge.verify import device_waveform
44
44
 
45
45
  logger = logging.getLogger(__name__)
46
46
 
@@ -146,7 +146,7 @@ def programme_levels_db(
146
146
 
147
147
  index = int(power.sum(axis=1).argmax()) if frame_index is None else frame_index
148
148
  anchor = int(coarse[index])
149
- half = int(round(LOUDEST_SPAN_S * fs / 2.0))
149
+ half = round(LOUDEST_SPAN_S * fs / 2.0)
150
150
  fine = np.arange(
151
151
  max(0, anchor - half),
152
152
  min(len(samples) - NPERSEG, anchor + half) + 1,
@@ -15,7 +15,7 @@ Two routes, matching the contract's `method`:
15
15
 
16
16
  import logging
17
17
  from collections.abc import Callable
18
- from dataclasses import dataclass
18
+ from dataclasses import dataclass, field
19
19
  from typing import Literal
20
20
 
21
21
  import numpy as np
@@ -118,7 +118,7 @@ class DesignParams:
118
118
  least often selected. `PipelineParams.fit_seeds` sets this now, and the
119
119
  reasoning there applies here unchanged."""
120
120
 
121
- realisation: Realisation | None = Realisation()
121
+ realisation: Realisation | None = field(default_factory=Realisation)
122
122
  """How the cascade will actually be realised, scored during the fit.
123
123
 
124
124
  On by default. Fitting in float64 alone accepts cascades built from large opposing
@@ -321,7 +321,7 @@ def _unpriced_fitted_target(
321
321
 
322
322
 
323
323
  def _nearest_even_order(order: float) -> int:
324
- return max(2, 2 * int(round(order / 2.0)))
324
+ return max(2, 2 * round(order / 2.0))
325
325
 
326
326
 
327
327
  PROTECTIVE_ALIGNMENT = Alignment.BUTTERWORTH
@@ -77,7 +77,7 @@ logger = logging.getLogger(__name__)
77
77
  CONTRACT_VERSION = "1.6"
78
78
  """The version this module implements. Responses echo the request's own version instead."""
79
79
 
80
- DESIGN_VERSION = 9
80
+ DESIGN_VERSION = 10
81
81
  """Which answer this code gives (1.4): bumped by any change that can alter `candidates`,
82
82
  `rejected`, a decline or a 1.3 fit figure for an unchanged request and unchanged settings.
83
83
 
@@ -105,7 +105,9 @@ the first the judge passes, fewest sections first, instead of whichever the arit
105
105
  domain (TODO 23), which re-rolls the fit among near-equal optima as any change to its
106
106
  arithmetic does. 0.3.2 is 9: it stops bass EQ at a 60 Hz ceiling: no target asks for lift
107
107
  above it, the judged band's top is capped there, and a cascade may add no more than the level
108
- tolerance beyond it (TODO 39). `plans/design-version.md` has each one's build."""
108
+ tolerance beyond it (TODO 39). 10 breaks a flatness tie between candidates by the strategy's
109
+ place in `STRATEGIES`, not by section count (TODO 32). `plans/design-version.md` has each one's
110
+ build."""
109
111
 
110
112
  Coverage = Literal["complete_programme", "excerpt"]
111
113
  ChannelScope = Literal["all_channels", "lfe_only", "mixed"]
@@ -966,14 +968,14 @@ def _validate_candidate(
966
968
  for i, spec in enumerate(candidate.filters):
967
969
  _validate_biquad_spec(spec, i, index, where)
968
970
  _validate_fit(candidate, f"{where}[{index}]")
969
- if candidate.commentary is not None:
970
- if not isinstance(candidate.commentary, dict) or not all(
971
+ if candidate.commentary is not None and (
972
+ not isinstance(candidate.commentary, dict)
973
+ or not all(
971
974
  isinstance(k, str) and isinstance(v, str)
972
975
  for k, v in candidate.commentary.items()
973
- ):
974
- raise ContractViolation(
975
- f"{where}[{index}].commentary must be a dict[str, str]"
976
- )
976
+ )
977
+ ):
978
+ raise ContractViolation(f"{where}[{index}].commentary must be a dict[str, str]")
977
979
 
978
980
 
979
981
  def _validate_fit(candidate: DesignCandidate, prefix: str) -> None:
@@ -150,8 +150,10 @@ def correction(candidate: Candidate, report: Report | None = None) -> str:
150
150
  for f in candidate.filters
151
151
  )
152
152
  parts = [
153
- f"{candidate.label}: {len(candidate.filters)} section(s) — {sections}; "
154
- f"peak boost {candidate.mv_adjust_db:+.1f} dB"
153
+ (
154
+ f"{candidate.label}: {len(candidate.filters)} section(s) — {sections}; "
155
+ f"peak boost {candidate.mv_adjust_db:+.1f} dB"
156
+ )
155
157
  ]
156
158
  recovered = candidate.verdict.recovered_fraction
157
159
  if math.isfinite(recovered):
@@ -168,8 +168,15 @@ def _block_bootstrap_se(
168
168
  their own isolated instant (wide) — a flat constant cannot see that difference.
169
169
 
170
170
  Vectorised across every bin in `bins` at once: the resampled *frame indices* are the same
171
- for every bin in a given replicate, so one `(n_boot, n_frames)` index matrix drives one
172
- gather and one `percentile` call rather than one per bin.
171
+ for every bin in a given replicate, so one `(n_boot, n_frames)` index matrix serves every
172
+ bin.
173
+
174
+ A replicate is a multiset of the masked frames, so its percentile needs only how often
175
+ each frame was drawn and the frames in order of value (`_bootstrap_percentiles`), not the
176
+ resampled values gathered and partitioned per replicate. That was 90% of `extract`, and
177
+ the LFE of a two-hour title (12 s) set a cold run's wall time; this takes about a sixth of
178
+ it and returns the same bits, which `tests/test_design_extraction.py` pins against the
179
+ gather.
173
180
  """
174
181
  runs = _runs(mask)
175
182
  if len(runs) < 2 or n_boot < 2:
@@ -195,21 +202,90 @@ def _block_bootstrap_se(
195
202
  if filled >= n_frames:
196
203
  break
197
204
  batch *= 2
198
- # Chunked over bins, because the gather below is the one large allocation here and it is
199
- # proportional to bins x replicates x frames: every bin of a two-hour title at 200
200
- # replicates is some 400 MB in one array. The arithmetic is identical either way — the
201
- # replicate indices are shared across bins by construction — so this only bounds peak
202
- # memory, and it is what lets the caller price every bin rather than a band of them.
205
+ percentiles = _bootstrap_percentiles(power, bins, mask, replicate_idx, percentile)
206
+ # C order, so `std` sums each bin's replicates in the order the gather's result did
207
+ percentiles_db = 10.0 * np.log10(np.ascontiguousarray(percentiles) + 1e-300)
208
+ return np.std(percentiles_db, axis=1)
209
+
210
+
211
+ _TOP_SHARE = 4.0
212
+ """How far down the values a replicate's percentile is sought, in multiples of its rank.
213
+
214
+ The 95th percentile of `n` frames is about the `0.05 n`-th largest, and a replicate draws each
215
+ frame about once, so the frames ranked within four times that hold it in all but freak draws.
216
+ A bin where some replicate needs more is sorted in full, so this sets only the speed."""
217
+
218
+
219
+ def _bootstrap_percentiles(
220
+ power: np.ndarray,
221
+ bins: np.ndarray,
222
+ mask: np.ndarray,
223
+ replicate_idx: np.ndarray,
224
+ percentile: float,
225
+ ) -> np.ndarray:
226
+ """Each replicate's `percentile` of each bin's power, `(bins, replicates)`.
227
+
228
+ `np.percentile`'s linear method over each replicate, without forming the replicate: the
229
+ order statistics it interpolates are read off the masked frames in descending order of
230
+ value, by how many draws each one carries, and interpolated as `np.percentile` does it
231
+ (`numpy.lib._function_base_impl._lerp`). The same values, the same arithmetic, so the same
232
+ bits. Non-finite power, which `np.percentile` treats its own way, takes the gather.
233
+
234
+ Chunked over bins to bound memory (`_BOOTSTRAP_ELEMENTS`): the counts gathered in sorted
235
+ order are replicates x bins x the frames searched.
236
+ """
237
+ n_boot, n_frames = replicate_idx.shape
238
+ frames = np.flatnonzero(mask)
239
+ position = np.zeros(mask.shape[0], dtype=np.int64)
240
+ position[frames] = np.arange(n_frames)
241
+ counts = np.zeros((n_boot, n_frames), dtype=np.int64)
242
+ np.add.at(counts, (np.arange(n_boot)[:, None], position[replicate_idx]), 1)
243
+ virtual = (n_frames - 1) * (percentile / 100.0)
244
+ lower = math.floor(virtual)
245
+ upper = n_frames - 1 if virtual >= n_frames - 1 else lower + 1
246
+ lower = min(lower, upper)
247
+ gamma = virtual - lower
248
+ # descending, the ascending order statistic j is where the draws passed exceed n - 1 - j
249
+ need_lower, need_upper = n_frames - 1 - lower, n_frames - 1 - upper
250
+ searched = min(n_frames, math.ceil(_TOP_SHARE * (need_lower + 1)) + 16)
251
+ chunk = max(1, _BOOTSTRAP_ELEMENTS // max(n_boot * searched, 1))
252
+ out = np.empty((bins.shape[0], n_boot))
253
+ for start in range(0, bins.shape[0], chunk):
254
+ taken = bins[start : start + chunk]
255
+ values = power[taken][:, frames]
256
+ order = np.argsort(-values, axis=1, kind="stable")[:, :searched]
257
+ passed = np.cumsum(counts[:, order], axis=2)
258
+ if not np.isfinite(values).all() or not (passed[:, :, -1] > need_lower).all():
259
+ out[start : start + taken.size] = _gathered_percentiles(
260
+ power, taken, replicate_idx, percentile
261
+ )
262
+ continue
263
+ top = np.take_along_axis(values, order, axis=1)
264
+ low = np.take_along_axis(top, (passed > need_lower).argmax(axis=2).T, axis=1)
265
+ high = np.take_along_axis(top, (passed > need_upper).argmax(axis=2).T, axis=1)
266
+ diff = high - low
267
+ out[start : start + taken.size] = (
268
+ high - diff * (1 - gamma) if gamma >= 0.5 else low + diff * gamma
269
+ )
270
+ return out
271
+
272
+
273
+ def _gathered_percentiles(
274
+ power: np.ndarray, bins: np.ndarray, replicate_idx: np.ndarray, percentile: float
275
+ ) -> np.ndarray:
276
+ """`_bootstrap_percentiles` the direct way: gather every replicate, `np.percentile` it.
277
+
278
+ Chunked over bins, because the gather is proportional to bins x replicates x frames:
279
+ every bin of a two-hour title at 200 replicates is some 400 MB in one array.
280
+ """
281
+ n_boot, n_frames = replicate_idx.shape
203
282
  flat_idx = replicate_idx.reshape(-1)
204
283
  chunk = max(1, _BOOTSTRAP_ELEMENTS // max(n_boot * n_frames, 1))
205
- out = np.empty(bins.shape[0])
284
+ out = np.empty((bins.shape[0], n_boot))
206
285
  for start in range(0, bins.shape[0], chunk):
207
286
  taken = bins[start : start + chunk]
208
287
  resampled = power[taken][:, flat_idx].reshape(taken.size, n_boot, n_frames)
209
- percentiles_db = 10.0 * np.log10(
210
- np.percentile(resampled, percentile, axis=2) + 1e-300
211
- )
212
- out[start : start + chunk] = np.std(percentiles_db, axis=1)
288
+ out[start : start + taken.size] = np.percentile(resampled, percentile, axis=2)
213
289
  return out
214
290
 
215
291
 
@@ -860,7 +860,7 @@ def _escalate(
860
860
  pending = [state for state in states if state.answer is None]
861
861
  if not pending:
862
862
  break
863
- tasks: list["FitTask"] = []
863
+ tasks: list[FitTask] = []
864
864
  owners: list[tuple[_Escalation, int]] = []
865
865
  for sections in group:
866
866
  for state in pending:
@@ -20,8 +20,8 @@ from dataclasses import dataclass
20
20
  import numpy as np
21
21
  from scipy import signal
22
22
 
23
- from beqforge.biquad import PeakingEQ
24
23
  from beqforge import Alignment, HighPass
24
+ from beqforge.biquad import PeakingEQ
25
25
  from beqforge.filters import high_pass_sos
26
26
 
27
27
  logger = logging.getLogger(__name__)
@@ -97,13 +97,13 @@ def synthesise(profile: SyntheticProfile, fs: float, seed: int = 0) -> np.ndarra
97
97
  can be reproduced from its profile and seed alone.
98
98
  """
99
99
  rng = np.random.default_rng(seed)
100
- n = int(round(profile.duration_s * fs))
100
+ n = round(profile.duration_s * fs)
101
101
  out = rng.standard_normal(n) * 10.0 ** (profile.floor_db / 20.0)
102
102
 
103
- count = max(1, int(round(profile.duration_s * profile.event_rate_hz)))
103
+ count = max(1, round(profile.duration_s * profile.event_rate_hz))
104
104
  starts = rng.integers(0, n, size=count)
105
105
  levels = 10.0 ** (-rng.uniform(0.0, profile.event_level_spread_db, count) / 20.0)
106
- decay_len = int(round(profile.event_decay_s * 6.0 * fs))
106
+ decay_len = round(profile.event_decay_s * 6.0 * fs)
107
107
  envelope = np.exp(-np.arange(decay_len) / (profile.event_decay_s * fs))
108
108
  nyquist = fs / 2.0
109
109
  for start, level in zip(starts, levels, strict=True):
@@ -284,6 +284,8 @@ FEATURES: tuple[Feature, ...] = (
284
284
  "pipeline:counterfactual_targets",
285
285
  "pipeline:_Restoration*",
286
286
  "pipeline:_restoration_mix",
287
+ "pipeline:_restoration_boosts",
288
+ "pipeline:_restored_target",
287
289
  "pipeline:PipelineParams.restore_caps_db",
288
290
  ),
289
291
  _strategy("counterfactual"),
@@ -557,6 +559,7 @@ FEATURES: tuple[Feature, ...] = (
557
559
  (
558
560
  "pipeline:run",
559
561
  "pipeline:Report*",
562
+ "pipeline:_strategy_rank",
560
563
  "pipeline:Candidate*",
561
564
  "pipeline:TargetFit*",
562
565
  "pipeline:target_fit",
@@ -163,3 +163,71 @@ basis_below = { no_fit = [0, 10], declined_after_fit = [3, 12], designed_with_fa
163
163
  feature = "judge.assess"
164
164
  kind = "unchanged"
165
165
  note = "the over-lift failure names the bass ceiling when its worst bin lies above it; wording only"
166
+
167
+ [[change]]
168
+ id = "tie-by-strategy"
169
+ design_version = 10
170
+ summary = "a flatness tie between passing candidates goes to the strategy listed first in `STRATEGIES`, not to the fewest sections, whose count a near-equal fit re-rolls with the arithmetic (TODO 32)"
171
+ evidence = "plans/library-review-improvements.md#a-stable-tie-break-2-november-2026-design_version-10"
172
+
173
+ [[change.touches]]
174
+ feature = "selection"
175
+ kind = "behaviour"
176
+ # Measured on the whole v8 library, not a sample, moved by `impact.MOVED` (another strategy
177
+ # wins, or another counterfactual cap): each record's candidates re-selected under both rules
178
+ # from their recorded verdicts and corrected curves. The old rule reproduces 1,068 of the 1,069
179
+ # recorded winners; the other is an exact tie (flatten and parametric 0.9355 dB, two sections
180
+ # each) that the new rule moves, counted here. A record with no passing candidate has no
181
+ # winner to move.
182
+ basis = { no_fit = [0, 20], declined_after_fit = [0, 16], designed_with_failures = [10, 271], designed_all_passed = [70, 762] }
183
+
184
+ [[change]]
185
+ id = "designer-latency"
186
+ design_version = 10
187
+ summary = "the designer computes what a run reuses once: the sub feed, the unfiltered spectra, each candidate's filtered waveform and device transfer, each channel's flat deficit and restored boosts, and the bootstrap percentiles off draw counts; no answer moves (TODO 23)"
188
+ evidence = "plans/designer-latency.md#validation-per-commit"
189
+
190
+ [[change.touches]]
191
+ feature = "analysis.blockers"
192
+ kind = "unchanged"
193
+ note = "`analyse` measures the mix's flat deficit once and `channels_missing_low_end` each channel's once; probe unchanged at --tol 0"
194
+
195
+ [[change.touches]]
196
+ feature = "analysis.extraction"
197
+ kind = "unchanged"
198
+ note = "bootstrap percentiles read off draw counts are bit-identical to the gathered ones; probe recomputing every analysis unchanged at --tol 0"
199
+
200
+ [[change.touches]]
201
+ feature = "fit.input"
202
+ kind = "unchanged"
203
+ note = "the over-lift allowance takes the run's sub feed and unfiltered spectra instead of rebuilding them; probe unchanged at --tol 0"
204
+
205
+ [[change.touches]]
206
+ feature = "judge.headroom"
207
+ kind = "unchanged"
208
+ note = "the headroom peak is interpolated in one-second blocks from the waveform verify filtered; real-run records identical, headroom peaks included"
209
+
210
+ [[change.touches]]
211
+ feature = "judge.verification"
212
+ kind = "unchanged"
213
+ note = "verify takes the run's sub feed, unfiltered spectra and one device transfer a candidate; probe unchanged at --tol 0, real-run records identical"
214
+
215
+ [[change.touches]]
216
+ feature = "targets.counterfactual"
217
+ kind = "unchanged"
218
+ note = "each distinct set of channel boosts is restored once across the caps, each channel's flat deficit measured once; probe unchanged at --tol 0"
219
+
220
+ [[change.touches]]
221
+ feature = "targets.flatten"
222
+ kind = "unchanged"
223
+ note = "the mix's flat deficit is measured once for the strategies; probe unchanged at --tol 0"
224
+
225
+ [[change.touches]]
226
+ feature = "targets.parametric"
227
+ kind = "unchanged"
228
+ note = "the mix's flat deficit is measured once for the strategies; probe unchanged at --tol 0"
229
+
230
+ [[change.touches]]
231
+ feature = "targets.shared"
232
+ kind = "unchanged"
233
+ note = "`propose`, the judged band and the passband ripple take the flat deficit measured once; probe unchanged at --tol 0"