Skip to content

Stop opening a deck from rewriting it - #25

Merged
Aduneer merged 1 commit into
mainfrom
lazy-card-ids
Aug 20, 2026
Merged

Stop opening a deck from rewriting it#25
Aduneer merged 1 commit into
mainfrom
lazy-card-ids

Conversation

@Aduneer

@Aduneer Aduneer commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Stacked on #24 — based on ci-macos so the diff shows only this change. GitHub will retarget it to main automatically when #24 merges. It shares the Unreleased CHANGELOG section and the golden transcripts with that PR, which is why it is stacked rather than parallel.

Studying any deck in examples/ straight from a clone left it modified in git status without a single card having been answered:

$ git clone https://github.com/Aduneer/FlashTerm && cd FlashTerm && make
$ ./FlashTerm examples/spanish.csv     # open it, quit immediately
$ git status --short
 M examples/spanish.csv

All twenty rows rewritten. That looks like the app corrupting its own sample data, it is an easy way to get surprise diffs, and it is the first thing a curious visitor does.

Two causes, both of which had to go

Cards were given an id when the deck was loaded. An id is what a log event names a card by, so a card needs one at the moment something is about to be recorded against it — and a deck that is only read has nothing recorded against it. Deck::ensure_id mints one there instead, from log_answer and log_undo.

The invariant that matters is unchanged: the id still exists before the event naming it is written. Golden cases that answer a card still show the deck's id matching the log's card_id.

Saving then expanded every row to its full width, so a hand-written question,answer,tags deck came back as question,answer,tags,0,0,1,,,<id>. Removing the minting alone would not have fixed anything — the file still differed.

card_to_csv now writes a card only as far as the last column it actually uses. That rule already existed for the audio and image columns for exactly this reason; it now covers the whole row instead of the last two. The values it trims against are the ones card_from_csv fills in for a column that is absent, so the round trip is unchanged — the two lists sit next to each other in the function with a comment saying they have to stay in step.

And a save that would change nothing is skipped

Every call site saves unconditionally — after every answer, after every edit, and on the way out. That is deliberate and is what makes an interrupted session cost nothing. It should not also mean that reading a deck counts as writing it, so Deck::save compares against what it read and returns success without touching the file.

A deck file that does not exist yet is still created, even when the deck is empty and serialises to nothing: the "have I seen the file" flag and an empty string are not the same question, and there is a test for exactly that.

Verification

$ ./FlashTerm examples/spanish.csv     # after
$ git status --short examples/
                                        # nothing

All eleven shipped decks confirmed byte-identical by md5 after being opened and closed. And from a fresh clone of this branch: make check passes and studying an example leaves the tree clean.

Six golden transcripts change, all cases that browse, tag, list, or quit without answering, all in the same direction — the deck fixture stays as it was written. Cases that answer a card are untouched.

New coverage: test_save_leaves_an_unchanged_deck_alone, and the id test now asserts the deferred behaviour rather than the old load-time minting.

Compatibility

Nothing about what a deck can contain has changed. Every deck an earlier version wrote still loads, and still saves identically once a card in it has been reviewed — a reviewed card has a non-default box, dates and id, so it writes the full row exactly as before. Only never-reviewed, id-less cards take the short form, and those are precisely the ones that came from a short-form file.

Checks

make check on gcc and clang, optimised and sanitised (-fsanitize=address,undefined -fno-sanitize-recover=undefined), the env -i clean room, and a fresh clone. 1201 unit checks, 47 golden cases.

Studying any deck in examples/ straight from a clone left it modified in
git status without a single card having been answered. That looks like
the app corrupting its own sample data, and it is an easy way to get
surprise diffs.

Two causes, both of which had to go.

Cards were given an id when the deck was loaded. An id is what a log
event names a card by, so a card needs one at the moment something is
recorded against it, and a deck that is only read has nothing recorded
against it. Deck::ensure_id mints one there instead. The id still exists
before the event naming it is written, which is the invariant that
matters.

Saving then expanded every row to its full width, so a hand-written
question,answer,tags deck came back as question,answer,tags,0,0,1,,,id.
card_to_csv now writes a card only as far as the last column it actually
uses. That rule already existed for the audio and image columns for
exactly this reason; it now covers the whole row instead of the last two,
and the defaults it trims against are the ones card_from_csv fills in for
a column that is absent, so the round trip is unchanged.

A save that would reproduce the file byte for byte is skipped outright.
Every call site saves unconditionally, which is what makes an interrupted
session cost nothing; it should not also mean that reading a deck counts
as writing it.

Six golden transcripts change, all of them cases that browse, tag, list
or quit without answering, and all of them in the same direction: the
deck fixture stays as it was written. Cases that answer a card are
untouched, and still show the deck's id matching the log's card_id.
@Aduneer
Aduneer deleted the branch main August 20, 2026 08:36
@Aduneer Aduneer closed this Aug 20, 2026
@Aduneer Aduneer reopened this Aug 20, 2026
@Aduneer
Aduneer changed the base branch from ci-macos to main August 20, 2026 08:36
@Aduneer Aduneer closed this Aug 20, 2026
@Aduneer Aduneer reopened this Aug 20, 2026
@Aduneer
Aduneer merged commit 7621117 into main Aug 20, 2026
6 checks passed
@Aduneer
Aduneer deleted the lazy-card-ids branch August 20, 2026 08:43
@Aduneer Aduneer mentioned this pull request Aug 20, 2026
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.

1 participant