What scrambled `a nissa one` took three defects at once. Reorder it on machine
A; open it on machine B and resize the window; B's file is now newer, so the
merge takes B's order — the old one — wholesale.
_merge_playlist was a 2-way union with no common ancestor: one side's order
wholesale, the other side's extras appended at the tail, so a track inserted in
the middle on one machine arrived at the bottom on the other. merge_track_order
re-inserts each side-only track after the nearest track both copies share
instead. _reconcile_playlist (the live-reload path) uses the same helper. The
union invariant is unchanged and property-tested over 300 random pairs — a
merge never drops a track.
Whose order wins is now Playlist.date_modified, bumped only in _set_track_ids
(add / remove / reorder / undo) and deliberately not by
mark_playlist_settings_dirty, with a file-mtime fallback for playlists written
before the field existed. The file's mtime was a lie: the last column is
stretch-sized, so Qt re-fires sectionResized whenever the viewport width
changes, and a window resize or splitter drag rewrote the open playlist's JSON
— moving its mtime and handing Syncthing another conflict — for a width
apply_settings overrides on load anyway. _on_section_resized now skips that
section; genuine drags on every other column still persist.
Replayed on a copy of the real playlists: `a nissa one` keeps its reorder
against a newer-by-mtime opponent, and `a nissa ideas` takes the other
machine's two mid-list inserts at 5 and 12 rather than 26 and 27.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8Mzze7shr5pZoEgKQU5NW