Delegate Coords and Spool task construction to the node layer - #48
Merged
Conversation
The Coords and Spool widgets were the last two that re-implemented node logic instead of delegating to it, so their canvas behavior could drift from a headless run of the same saved parameters. Coords: - CoordsTask validates eagerly and executes lazily (_validated_call); preflight(patch) runs the exact validation run() would, raising a structured CoordsValidationError (kind/label/detail). - The widget builds its task only via NODE_SPEC.build_task and maps preflight failures onto its existing banners; the four widget-side validator copies (mapping, selection, transpose, set-coords) are gone. - resolve_set_coord is shared by run() and the widget's draft validation. - coords_task_from_params promotes set_coords draft fields the way canvas rehydration does (drafts win; applied mirror used when no draft), so a headless caller filling only the draft fields no longer gets a silent no-op. - Data-flip of a non-dim coordinate now reports through invalid_selection at preflight instead of operation_failed at runtime. Spool: - New apply_spool_transforms and parse_spool_scalar in nodes/spool.py are the single definition of the select -> chunk stages, used by both node tasks and the widget's snapshot executor and display recompute. - A chunk value parsing to None (e.g. the text 'None') now disables chunking headlessly, matching the canvas chunk controls. - Deleted the widget copies: _apply_chunk_transform, _apply_select_transform, _parse_chunk_scalar, _iter_active_select_filters, and the test-only _snapshot_loader and _load_from_example; their tests now target the node layer. New Qt-free tests in tests/test_nodes/test_coords.py and additions to tests/test_nodes/test_spool_node.py.
5 tasks
d-chambers
added a commit
that referenced
this pull request
Aug 4, 2026
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
added a commit
that referenced
this pull request
Aug 4, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Continues the canvas/headless parity work from #47: Coords and Spool were the last two widgets that re-implemented node logic instead of delegating to it, so the task that ran on the canvas could differ from the task exported into a saved workflow, and a headless run could diverge from the canvas. After this PR the node layer is the single owner of task construction and validation for both.
Coords
CoordsTasknow validates eagerly and executes lazily:preflight(patch)runs exactly the validationrun()would, raising a structuredCoordsValidationError(kind/label/detail). The widget builds its task only viaNODE_SPEC.build_task, preflights it, and maps failures onto its existing error banners — its four duplicated validators (mapping, selection, transpose, set-coords) are deleted, along with the eight-branch hand-built task constructor. Canvas-run and exported tasks are now always identical.resolve_set_coordis a shared free function used byrun()and the widget's draft validation, so both accept and reject exactly the same set-coords inputs.coords_task_from_paramspromotes theset_coordsdraft fields the way canvas rehydration does (drafts win; the*_applied_*mirror is used only when no draft is present). A headless author filling onlyset_coords_dim/start/stop/steppreviously got a silent no-op; now they get the coordinate update.Spool
apply_spool_transforms+parse_spool_scalarinnodes/spool.pyare the single definition of the select → chunk pipeline, used bySpoolTask.run,SpoolTransformTask.run, the widget's snapshot executor (both settings and input-spool modes, previously two near-identical 30-line blocks), and the display recompute.None(e.g. the literal textNone) now disables chunking headlessly — previously it chunked with aNonevalue while the canvas no-opped. Ellipsis (...) parsing is preserved (ast.literal_evalhandles it)._apply_chunk_transform,_apply_select_transform,_parse_chunk_scalar,_iter_active_select_filters, and the test-only_snapshot_loader/_load_from_example; their tests now exerciseload_spool_from_settingsand the shared transforms at the node layer.Net −180 lines in
src/. CHANGELOG updated under Unreleased → Fixed.Counterpart review: the Codex CLI was unavailable (usage limit until Aug 8), so no counterpart review was run; an adversarial self-review record covering the same risk areas (closure late-binding, validation ordering, draft-promotion precedence, Ellipsis parsing, settings-sync freshness) is in the untracked
.scratch/review_node_delegation.md. That self-review caught and fixed one real issue: the draft-promotion precedence initially preferred the applied mirror over drafts, which contradicts canvas rehydration semantics.Checklist
I have (if applicable):
tests/test_nodes/test_coords.py; node-level source-routing/chunk-parsing tests intest_spool_node.py; widget tests repointed at the delegated paths).prek run --all-filesandpytest tests(2106 passed, 44 skipped; one unrelated canvas test flaked once intest_orange_qt.pyand passes in isolation, with its file, and on a full-suite rerun).python scripts/bench_compare.py --baseline main(table below) and added thebenchmarklabel.Benchmarks
Full suite (core + qt) vs
origin/main— threshold 20%: 0 slower, 0 faster, 33 compared, control −6.1% (ok):The worst median (+8.0%, waterfall default-levels) is in code this PR does not touch and its min-delta is +1.9% — machine noise, not a regression.