- Add label nodes at top of each chord column
- Modify xforce layout to skip label nodes
- Click label to play entire chord on siren
- Use chordIndex/nodeIndex (server calculates frequency)
- Fix server to support both calling conventions
- Fix client to use proper JSON headers for kill-siren
- Compute sum of harmonic distances between all pitch pairs
- Lower sum = more compact chord = higher probability
- Uses pitch_difference() to get diff, then log2(n*d) for distance
- Add CLI arg: --weight-harmonic-compactness
- Add toggle button and 'f' keyboard shortcut
- Calculate frequencies on-demand using fundamental * fraction
- Use direct style updates for reliable label refresh
- Add /api/play-siren endpoint for Shift+click
- Add /api/kill-siren endpoint for k/K keys
- Add set_chords method to OSCSender for dynamic voice count
- Update soft kill to 20Hz in osc_sender.py
- Refactor CLI to use send_kill method
- Fix kill to send to all voices regardless of count
- Add /api/set-fundamental and /api/play-freq endpoints to server.py
- Add send_single method to osc_sender.py for sending single frequencies
- Add fundamental frequency input and click handler to path_navigator.html
- Distinguish between click (play) and drag (move) on nodes
- Add osc_receiver.scd for SuperCollider to receive /freq messages
- Server calculates graph edges and ratios using src/pitch.py
- Frontend fetches computed graph from API
- Supports loading different output files via dropdown
- All pitch calculations now in Python, JS only for rendering
- Add symdiff to edge tuple in _find_valid_edges() and store in graph
- Add --uniform-symdiff CLI option to make symdiff selection uniform
- Implement uniform selection in pathfinder by grouping edges by symdiff
- With uniform: path uses symdiff 2 and 4 roughly equally
- Without uniform: path uses mostly symdiff 4 (most common)
- Add _is_adjacent() to check if pitches differ by ±1 in one dimension
- Add _compute_dim_diff() to compute prime * direction for dimension changes
- Add _find_ref_and_dim_diff() to find the ref voice and dim_diff for changed pitches
- Update output_chords_to_music_data() to compute ref and dim_diff by comparing consecutive chords
- Update format_dim_diff() to include dim_diff == 0 check and fix ref mapping for 4 voices
- Compute both c1→c2 and c2→c1 edges independently using _find_valid_edges()
- Fix _is_directly_tunable to properly reorder c2_transposed using movement map
- Clean up unused valid_pairings code in _build_movement_maps
- Add edge_data field to PathStep for debugging