Skip to content

refactor: dedupe command, client and Remote Script layers without moving the public surface - #130

Merged
6uclz1 merged 10 commits into
mainfrom
claude/inspiring-sagan-6tzk45
Oct 23, 2026
Merged

6uclz1 merged 10 commits into
mainfrom
claude/inspiring-sagan-6tzk45

Conversation

@6uclz1

@6uclz1 6uclz1 commented Oct 23, 2026

Copy link
Copy Markdown
Owner

What this changes for you

Nothing on the command line or the wire. Command names, options, help text, JSON envelopes, exit codes and the TCP protocol are unchanged. The command surface snapshot, the public contract snapshot, the generated skill docs and the generated client all regenerate with no diff.

The work cuts duplicated code. Synth and effect behaviour now lives in one place in the CLI and one place in the Remote Script, instead of two copies that could drift apart. 43 files changed, +1759 / −3117. Quality harness warnings fall from 447 to 416 and duplicate groups from 97 to 66, with 0 failures.

Commits, in order

  1. Subtract. Deletes seven require_* validators that only their own tests called, and the unread CLIENT_METHOD_SPEC_BY_NAME. Its import-time duplicate check becomes a test.
  2. device commands. Seven commands that take only the track and device selectors repeated the same 13-option signature. They now come from one registration factory.
  3. synth / effect commands. The two modules were parallel copies. commands/_device_family.py builds both groups from a DeviceFamily. find stays in each module because its --type parameter name (synth_type, effect_type) is part of the command surface snapshot.
  4. Shared Typer parameter types. TrackArgument, ClipArgument, ProjectOption and the note source and filter options were spelled out at about 150 call sites. They now live in commands/_params.py, applied by a codemod. Clip and arrangement notes get / notes clear each come from one filter-command factory.
  5. Client. Three hand-written pass-through setters move into CLIENT_METHOD_SPECS, so the generator writes them. The clip and arrangement note-args builders become one _note_args.
  6. Pin, then the Remote Script change. No test asserted the messages or hints on the synth/effect error paths, so a characterization test lands first. It passes on the old code and on the new. Then device_families.py replaces the second copy of each step: the supported-device check, the parameter payload, standard key resolution, seven backend methods and seven handlers. The handler factory passes each backend method as an explicit attribute, so test_backend_protocol_has_no_unused_methods still sees every call.
  7. Harness. quality_harness/globs.py replaces two identical _expand_globstar copies. The quality harness baseline is regenerated, and the client spec table is kept in name order.

How it was verified

  • ruff check, ruff format --check, the full pytest suite (1435 passed), the quality harness with the baseline (exit 0), and the Remote Script Python 3.11 compatibility check.
  • All four generators rerun with no diff.
  • ableton-cli --help, --version and python -m build pass. The wheel contains device_families.py.
  • Old vs new runs against 2b1921d:
    • Remote backend: 156 synth/effect calls, 63 of them error paths, with byte-identical results.
    • CLI: 20 invocations through ableton_cli.cli:main, covering argument-validation failures and unreachable-Live failures. Identical JSON envelopes and exit codes.
  • Independent review on a second model. It ran its own comparisons: a recursive help and param dump of the whole CLI (7081 lines), 253 handler cases and 81 CLI-to-client calls. All identical, apart from the call style noted below.

Not run: a manual check against a running Ableton Live. One synth and one effect command on a real device is worth a minute before merging.

For the reviewer

  • Call style. _device_family.py looks up client methods by string template, as CommandSpec.client_method already does across the CLI. The Remote Script side uses explicit attributes instead, because a parity test reads them statically. The one small call-style change is that synth <type> keys now passes synth_type by keyword rather than position. The real client's parameter has that name.
  • Baseline. The net warning count fell, but the regenerated baseline also takes in drift that was already on main. The live count there was 447 against 428 baselined. About 8 new entries come from _device_family.py itself, from its Typer signatures.
  • Left as is. The remaining duplicate groups were left on purpose:
    • generated code;
    • typed client methods;
    • CLI and Remote Script validators that cannot share a package;
    • the per-module run_client_command wrappers. Tests monkeypatch get_client on each module, so these wrappers are how the tests inject their fakes.

Generated by Claude Code

claude added 10 commits October 23, 2026 04:01
Seven require_* helpers in commands/_validation.py had no caller outside
their own tests, and CLIENT_METHOD_SPEC_BY_NAME was built only for its
import-time duplicate check. The volume and pan tests now pin the helpers
that commands actually call, and the duplicate check is a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
Seven device commands (parameters list, active get, preset list,
sidechain get, delete, chains list, macro list) repeated the same
13-option signature and body. They now come from
_register_device_target_command, and macro set uses the shared
_run_device_target_command. The command surface snapshot is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
synth.py and effect.py were parallel copies that differed only in the
family name, the wrapped device types and the client method names, which
all follow the {family} pattern. _device_family.py now builds both
groups from a DeviceFamily. Only find stays per module, because its
--type parameter name (synth_type, effect_type) is part of the command
surface snapshot.

The adapter tests for the deleted per-module helpers move to the shared
run_track_device_command; the unused validators hook goes with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
TrackArgument, ClipArgument, ProjectOption and the note source and
filter options were spelled out in full at 150 call sites across 14
modules. They now live in commands/_params.py. With the signatures
shared, clip notes get/clear and arrangement clip notes get/clear each
come from one filter-command factory.

The help text and parameter shapes are unchanged; the command surface
snapshot is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
set_device_parameter, set_synth_parameter_safe and
set_effect_parameter_safe were hand-written pass-throughs, which is
exactly what CLIENT_METHOD_SPECS generates; they now come from the
spec table and _call_parameter_command is gone. The clip and
arrangement note-args builders differed only in the clip key and are
now one _note_args.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
No test asserted the message or hint of the family error paths
(unsupported device, type mismatch, unknown key, unknown type name).
These pass on the current code and hold the contract for the next
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
The Remote Script carried two copies of every synth/effect step: the
supported-device check, the parameter payload, the standard key
resolution, seven backend methods and seven handlers. A DeviceFamily
table now holds what differs (type list, canonicalizer, detector, key
catalog, unsupported-device hint) and one implementation reads it.

The public backend methods and handler names are unchanged. The
handler factory takes each backend method as an explicit attribute so
the CommandBackend parity test still sees every call. An old-vs-new
run of 156 backend calls (63 of them error paths) gives byte-identical
results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
runner.py and dependencies.py each carried an identical
_expand_globstar. It now lives in quality_harness/globs.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
The accepted debt went down: 428 baselined warnings become 416, with 0
failures. On main the live count had drifted to 447, 19 over the
baseline and one short of the fail threshold for warning_delta. The new
baseline records the current state so the next regression is caught
from here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
The three parameter-setter specs added earlier sat before set_clip_name;
the table is documented as sorted by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159HyNwc4JV7ocTay3N3KL4
@6uclz1

6uclz1 commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

Live verification (Ableton Live 12.4.6, macOS)

The PR's Remote Script (0e008fa) was installed with install-remote-script --yes and loaded by a fresh Live launch. I loaded Wavetable and Utility onto one MIDI track of a new set and ran the commands below.

Preflight: wait-ready and doctor passed (8/8). All 175 commands are present and the command set hash is unchanged.

Synth

  • synth parameter set on Wavetable Osc 1 Transp: 0 → 7 → 0. before and after were correct.
  • synth find (--type wavetable / meld), synth parameters list, synth observe and synth wavetable keys returned OK.
  • Error paths returned the expected INVALID_ARGUMENT messages and hints:
    • out-of-range value (value must be between -24.0 and 24.0)
    • a device that is not a synth (Utility)
    • type mismatch (synth drift set on Wavetable)
    • device index out of range
  • --read-only with a write returned READ_ONLY_VIOLATION.

Effect

  • effect parameter set on Utility Balance: 0 → 0.5 → 0. before and after were correct.
  • effect find --type utility, effect parameters list, effect observe and effect utility keys returned OK.
  • Error paths returned the expected messages and hints:
    • out-of-range value
    • a device that is not an effect (Wavetable)
    • type mismatch (effect eq8 set on Utility)

CLI parity: Before the Remote Script swap, I ran 14 read-only synth/effect commands from both the main CLI and the PR CLI against the same running Live. stdout and exit codes were byte-identical.

Afterwards I reinstalled main's Remote Script.

Existing issues on main, not caused by this PR

These behave the same with main's Remote Script, and this PR does not touch synth_specs.py or effect_specs.py.

  • synth wavetable observe/set fails with Missing required standard synth keys for wavetable: filter_cutoff, filter_resonance. Live 12.4.6 names these parameters Flt 1 …, for example Flt 1 Res, not Filter 1 Freq.
  • effect utility observe/set fails with Missing required standard effect keys for utility: gain.
  • browser search is rejected under --read-only (search_browser_items is classified as a write command).

@6uclz1
6uclz1 merged commit f30c30f into main Oct 23, 2026
11 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.

2 participants