v0.13.0: the merge report says who, what, and where

The merge window kept reporting "Order kept from this machine (most recently
edited)" for playlists edited on the other machine. Three defects, confirmed
against the real snapshots in .resolved/:

* "this machine" was inferred from which copy held the plain filename. That is
  Syncthing's call, not a statement about authorship — it sets the local copy
  aside as readily as a remote one. In the 9:34 PM `* a fresh master` merge the
  copy labelled "the other machine" was this machine's own 3:15 PM merge output,
  so the label was exactly backwards. The 7-char device ID in the conflict
  filename — the only real evidence — was matched by a bare \w+ and deleted with
  the file. New sync_identity.py decodes it against Syncthing's config.xml and
  works out which device is us from cert.pem.

* The decision leaned local. date_modified was only consulted when *both* copies
  had one, and an iTunes playlist never reordered here has none — so the honest
  comparison was skipped exactly when one machine had edited and the other
  hadn't. A stamped copy now beats an unstamped one; mtime is the fallback only
  when neither side has ever been edited. And every merge used to rewrite the
  file it kept whether or not anything changed, freshening its mtime while the
  conflict file kept its origin's: a ratchet. No-op merges write nothing, and a
  merge whose result is a union neither copy had stamps date_modified, so the
  other machine adopts it instead of trading the same 19 tracks back and forth.

* Nothing was actionable. Re-inserted tracks are now named with their position
  ("Pola — Abeille -> position 24, after ..."), six in the window and all of
  them in what-changed.txt at the top of the backup snapshot, alongside the real
  conflict filename and its device. Tracks only this copy has are reported too
  rather than resurrected in silence.

Also fixed while in here: a rename or folder move made elsewhere was discarded
by every merge (only track_ids and settings were adopted); _reconcile_playlist
asserted the local edit was newer and never checked, so a reorder synced in from
the other machine was undone and flushed back to disk, and the branch reaching
it was gated on a dirty flag that a column drag sets; and _merge_metadata
decided the music folder from whichever copy an mtime coin flip had kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9ZEFi4qNJ39FMiBtiAxy2
This commit is contained in:
2026-08-27 15:16:49 -04:00
co-authored by Claude Opus 5
parent 37b3744aa4
commit c8de124543
10 changed files with 978 additions and 71 deletions
+79
View File
@@ -1,5 +1,84 @@
## Done
### Round 43 (2026-08-27) — The merge report says who, what, and where (v0.13.0)
The merge window kept saying `Order kept from this machine (most recently
edited)` for playlists trav had edited on the *other* machine, and offered a
backup folder holding two directories with one hex-named JSON each. Confirmed
against the real snapshots in `.resolved/`: in the 9:34 PM `* a fresh master`
merge, the copy labelled "the other machine" was this machine's own 3:15 PM
merge output (2472 tracks, `date_modified 19:15:26` — the previous merge's own
stamp), so the label was exactly backwards.
- [x] **Stop guessing which machine wrote which copy.** "this machine" was
inferred from which copy held the plain filename, and that is Syncthing's
call, not a statement about authorship — it sets the local copy aside as
readily as a remote one. The report now names the two copies for what
they factually are ("the copy that was already here" / "the copy
Syncthing set aside") and attributes the set-aside one from the 7-char
device ID in the conflict filename, which `CONFLICT_PATTERN` used to
match with a bare `\w+` and delete along with the file. New
`lintunes/sync_identity.py` maps that token to a device name out of
Syncthing's `config.xml` and works out which one is us by deriving our own
device ID from `cert.pem` (base32 of the SHA-256 of the DER cert, the way
Syncthing does). Stdlib only, best-effort: no Syncthing, no config or an
unreadable cert all mean the report simply names nobody.
- [x] **`date_modified` is consulted when only one copy has one.** It used to
need *both* stamps (`if ours and theirs`) and otherwise fell back to file
mtime — and a playlist imported from iTunes and never reordered on this
machine has no stamp at all, so the honest comparison was skipped exactly
when one machine had edited and the other hadn't. A stamp only exists once
LinTunes recorded an edit, so stamped-vs-unstamped is evidence: the
stamped copy wins. mtime is the fallback only when neither side has ever
been edited, and the report says so when it happens.
- [x] **No more mtime ratchet.** `_merge_playlist` rewrote the file it kept on
every merge, whether or not anything changed, while Syncthing preserves
the origin's mtime on the conflict file — so each merge made the copy in
place harder to beat on the next one. A no-op merge now writes nothing. A
merge that produces a true union stamps `date_modified`, because content
neither copy had is genuinely newer than both; that is what stops two
machines trading the same 19 tracks back and forth (three times in one
evening, in the snapshots).
- [x] **The report names the songs.** `merge_track_order` knew every
re-inserted track's position and threw it away. Each re-inserted track is
now listed as `Artist — Title → position 24, after "…"`, six in the window
and all of them in the backup. Titles come from a lazy read of
`library.json` (the 15 MB file, so only when a playlist merge actually
needs a name).
- [x] **Tracks the other copy didn't have are reported, not resurrected in
silence.** The union policy is unchanged — nothing is ever removed — but a
track only this copy has is now named, with both readings offered ("if you
added it here, that's all this is; if you deleted it on the other machine,
delete it here too"), since two lists give no way to tell which it was.
- [x] **`what-changed.txt` in the backup folder.** The same merge in words,
uncapped, beside `original/` and `incoming/` — including the real
Syncthing conflict filename and its device, which the merge otherwise
destroys. The dialog button is now "Open merge report".
- [x] **A rename or folder move made elsewhere survives a merge.** Only
`track_ids` and `settings` were ever taken from the winning copy, so a
playlist renamed on the other machine was renamed back by every merge.
`name` and `parent_persistent_id` come from the winner now, and
`_apply_rename`/`_apply_reparent` stamp `date_modified` so a rename is
comparable across machines at all.
- [x] **The same revert, on the sync path with no conflict file.**
`_reconcile_playlist` asserted in its docstring that the local edit was
the more recent one and never checked — so a reorder synced in from the
other machine was undone and then flushed straight back to disk. It reads
the stamps now, and adopts `name`/`parent`/`settings` when the disk copy
wins. The branch that reaches it is also gated properly: `_dirty_playlists`
is set by a column drag or a sort click too, so a sync landing in the 3 s
after a cosmetic click routed through the keep-local path. New
`_dirty_playlist_content` marks only real content edits — the Round 42
rule ("cosmetic table settings must not look like edits") one level up.
- [x] **The music folder no longer rides a coin flip.** `_merge_metadata`
inferred which copy had lost from whichever one `_keep_newer`'s mtime pick
had kept; when both files land in the same instant that pick is arbitrary,
which is why `test_but_adopting_a_music_folder_is_a_change` passed about
half the time. It decides from the two inputs' `music_folder_set_at`
directly, and grades CHANGE when the folder actually differs from the one
we started with.
### Round 42 (2026-08-22) — A web mix stops shipping an .m3u (v0.11.1)
- [x] **No `.m3u` beside `index.html`.** It shipped on the theory that it cost