Absorb sync-conflict copies of the review log - #20
Merged
Conversation
merge_events() and replay() have been written, tested and unused since #8. This is the command that calls them, and it is deliberately the smallest one that could: no --sync, no git, no network. When two machines both append to deck.txt.log before they sync, the sync client cannot merge the two versions, so it keeps one and parks the other beside it as deck.txt.sync-conflict-20260818-101112-K3JQ7ZP.log. Neither copy is wrong -- the log is append-only, so they are two halves of one history. `FlashTerm deck.txt --absorb-conflicts` unions them by event id, rewrites the log in timestamp order, and brings the deck's counters and due dates up to date with the reviews it just gained. The question that kept replay() unwired -- how replayed state should meet counters that predate the log -- is answered by never trusting replay absolutely. The log is replayed twice, before the merge and after it, and only the difference is applied. Counters gain exactly what the other machine recorded; a card whose history began before the log keeps all of it. Whether the merged log gets to set a card's schedule turns on whether this machine's log already covered that card: if it did, replay is the whole story for it and following it backwards is right too, which is what makes an absorbed *undo* roll a card back rather than being silently ignored. Three deliberate choices worth stating: * The conflict copies are read and left alone. Absorbing is idempotent -- a second run finds nothing new -- so leaving them costs only disk, while deleting the wrong file costs a history that exists nowhere else. The output ends with the `rm` line, and lets the user run it. * Which names count is one predicate, not a list of vendors. Syncthing, Dropbox and Nextcloud each insert their marker in a different place, so both "before the extension" and "appended" are accepted, and the inserted text has to contain the word "conflict". That keeps deck.txt.log.bak out, and the .tmp of an interrupted rewrite -- absorbing that one would be circular. * Events naming cards the deck does not have are kept and reported rather than dropped. The deck and the log are two files and nothing makes a sync client deliver them in step. EventLog gained rewrite(), the temp-file-and-rename twin of Deck::save(). An append is atomic in practice; replacing a whole log is not, and the log is the only copy of what happened. The golden harness gained a files/ fixture directory, copied in by content: a log and the conflict copies beside it have names the sync client invents, which the fixed audio/ and voices/ names cannot express. .gitignore needed a matching exception, since *.log would otherwise have dropped the new fixtures on the floor and failed CI while passing here. Four new golden cases and 76 new unit checks: the naming predicate including every negative, discovery and its ordering, the atomic rewrite and its failure path, the differential replay with a pre-log card that must not move, an absorbed undo, idempotence, and the two usage errors. make check is 618 unit checks and 42 golden cases, green on gcc and clang with -Werror, clean under ASan/UBSan, and clean in an `env -i` clean room.
Aduneer
added a commit
that referenced
this pull request
Aug 19, 2026
Pictures on cards, sync that puts two machines' reviews back together, and a CI matrix that checks what used to be checked by hand. PRs #20-#23. The CHANGELOG entries gained their PR numbers, which the file's own preamble promises and these four had been written without. No transcript changed for the version bump, which is what the <VERSION> placeholder in normalise.awk exists for: releasing does not touch 47 expected files. The demo GIF is already current -- it was re-rendered in #22, and doing so is what caught the two terminal-detection bugs that release fixes.
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.
merge_events()andreplay()have been written, tested and unused since #8. This is the command that calls them, and it is deliberately the smallest one that could: no--sync, no git, no network.When two machines both append to
deck.txt.logbefore they sync, the sync client cannot merge the two versions, so it keeps one and parks the other beside it asdeck.txt.sync-conflict-20260818-101112-K3JQ7ZP.log. Neither copy is wrong — the log is append-only, so they are two halves of one history.How replayed state meets counters that predate the log
This is the question that kept
replay()unwired, and it is answered by never trusting replay absolutely. The log is replayed twice, before the merge and after it, and only the difference is applied. Counters gain exactly what the other machine recorded; a card whose history began before the log keeps all of it.Whether the merged log gets to set a card's schedule turns on whether this machine's log already covered that card. If it did, replay is the whole story for it and following it backwards is right too — which is what makes an absorbed undo roll a card back rather than being silently ignored. If it did not, the deck's own state wins when it is newer, because an older event is not evidence about today's schedule however new it is to this machine.
Three deliberate choices
rmline and lets the user run it.conflict. That keepsdeck.txt.log.bakout, and the.tmpof an interrupted rewrite — absorbing that one would be circular.Also in here
EventLoggainedrewrite(), the temp-file-and-rename twin ofDeck::save(). An append is atomic in practice; replacing a whole log is not, and the log is the only copy of what happened.The golden harness gained a
files/fixture directory, copied in by content: a log and the conflict copies beside it have names the sync client invents, which the fixedaudio/andvoices/names cannot express..gitignoreneeded a matching exception, since*.logwould otherwise have dropped the new fixtures on the floor and failed CI while passing locally — checked from a fresh clone, not assumed.Testing
Four new golden cases and 76 new unit checks: the naming predicate including every negative, discovery and its ordering, the atomic rewrite and its failure path, the differential replay with a pre-log card that must not move, an absorbed undo, idempotence, and the two usage errors.
make checkis 618 unit checks and 42 golden cases, green on gcc and clang with-Werror, clean under ASan/UBSan, and clean in anenv -iclean room.