Skip to content

Finish the canvas/headless parity punch list - #49

Merged
d-chambers merged 1 commit into
mainfrom
fix/parity-punchlist
Aug 4, 2026
Merged

Finish the canvas/headless parity punch list#49
d-chambers merged 1 commit into
mainfrom
fix/parity-punchlist

Conversation

@d-chambers

Copy link
Copy Markdown
Contributor

Description

Closes out the canvas/headless parity theme from the architecture review (following #47 and #48) — the three remaining small divergences:

SelectSelectionState.apply_select_params now accepts patch=None and builds the display-only state itself, replacing the widget's hand-built copy of that branch (duplicate basis derivation and field-by-field state assembly). Note: the review's original "payload writer lives only in the widget" finding had already been resolved in the meantime — patch_settings_payload lives on the model — so this closes the last remaining duplication.

Filter — the boundary-mode whitelists moved to nodes/filter.py as FilterMode/SavgolFilterMode Literals, and the params models now validate them: a headless SavgolFilterParams(mode="reflect") (which SciPy rejects) fails at validation with a clear message instead of surviving until the SciPy call. The widget's combo options are derived from the same Literals via get_args, so UI and validation cannot drift. Deliberately not moved: the legacy Gaussian-settings migration — it handles the pre-row-list flat-settings shape, which cannot reach the typed params layer at all (pre-_state workflows load as defaults per the documented storage break), so there is no headless path to diverge.

PlayAudio — the node was hollow: it declared time_scale/volume_percent params that nothing consumed while all the Qt-free DSP sat in the widget. Everything between a patch and device-ready PCM (shape validation, rate inference, robust-gain PCM normalization, default time scale, volume→dB mapping, output-rate clamping, block-wise resampling) now lives in nodes/playaudio.py; only the QAudioSink/QAudioFormat half remains in the widget. The params are now actually consumed: the new render_audition(patch, params) returns (prepared, output_rate_hz, pcm) headlessly.

CHANGELOG updated under Unreleased → Fixed.

Counterpart review: the Codex CLI is still rate-limited (verified again before this PR; resets Aug 8), so no counterpart review was run; the adversarial self-review record is in the untracked .scratch/review_parity_punchlist.md (covers the apply_select_params behavior-preservation check, why legacy blobs don't gain a new failure mode from the Literals, the deliberate non-move of the Gaussian migration, and a repo-wide stale-reference sweep after the PlayAudio move).

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented new behavior with docstrings and a CHANGELOG entry.
  • included tests (new tests/test_nodes/test_playaudio.py; mode-validation tests in test_filter_params.py; the PlayAudio DSP unit tests now target the node functions).
  • run prek run --all-files and pytest tests (2114 passed, 44 skipped; all 10 hooks pass).
  • for hot-path changes, run python scripts/bench_compare.py --baseline main. (not run — no hot path is touched: the moves are logic-identical relocations, the Literal validation replaces same-cost str validation, and PlayAudio DSP is not benchmark-covered)

Close out the three remaining small divergences from the architecture
review (following #47 and #48):

Select:
- SelectionState.apply_select_params now accepts patch=None and builds
  the display-only state itself; the widget's hand-built copy of that
  branch (basis derivation + field-by-field state assembly) is gone.

Filter:
- The mode whitelists moved to nodes/filter.py as FilterMode /
  SavgolFilterMode Literals; the params models validate them, so a
  headless SavgolFilterParams(mode='reflect') fails at validation with
  a clear message instead of inside SciPy. The widget's combo options
  are derived from the same Literals via get_args, so UI and validation
  cannot drift.
- The legacy Gaussian-settings migration stays widget-side on purpose:
  it handles the pre-row-list flat-settings shape, which cannot reach
  the typed params layer (pre-_state workflows load as defaults).

PlayAudio:
- All Qt-free DSP moved to nodes/playaudio.py: patch validation, rate
  inference, PCM normalization (prepare_pcm_audio), default time scale,
  volume-to-dB mapping, output-rate clamping, and playback resampling.
  Only the QAudioSink/QAudioFormat half remains in the widget.
- The node's time_scale/volume_percent params are now consumed: the new
  render_audition(patch, params) returns device-ready PCM headlessly.

New tests: tests/test_nodes/test_playaudio.py (params-driven audition),
mode-validation tests in test_filter_params.py; the DSP unit tests now
target the node functions.
@d-chambers
d-chambers force-pushed the fix/parity-punchlist branch from 027a70a to d5022d5 Compare August 4, 2026 14:00
@d-chambers
d-chambers merged commit d5d8689 into main Aug 4, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant