Summary
mark_done_many writes the ledger through atomic_write_text (deferredwork.py:354) with a
documented rationale — a bare tmp+replace resets the file mode and turns a symlinked ledger into a
regular file. Its two sibling writers do not:
append_decision — deferredwork.py:389: path.write_text(text, encoding="utf-8")
append_entry — deferredwork.py:492: path.write_text(text + sep + block, encoding="utf-8")
Path.write_text opens with mode 'w' (truncate) and then encodes. Any failure between those two
points — an unencodable value, ENOSPC, EIO — leaves a zero-byte ledger. Every deferred-work
entry is gone.
Reproduced during review: an append_decision call that raised mid-write left a 35-byte ledger at
0 bytes, while the same value through mark_done left it intact.
Why the asymmetry matters
mark_done_many's docstring already argues the case for this exact file — "swapping a fresh inode
over the ledger otherwise resets its mode (a 0600 ledger silently becoming world-readable) and
turns a symlinked ledger into a regular file". That reasoning applies unchanged to the other two
writers; the protection just was not extended to them.
An asymmetric guard between siblings is itself the finding: a reader cannot tell whether the
difference is deliberate.
Fix shape
Route append_decision and append_entry through atomic_write_text too. Note append_entry
creates the file when absent (path.parent.mkdir(...)), so the helper needs to keep working for a
not-yet-existing target.
Found during the #305 review.
Summary
mark_done_manywrites the ledger throughatomic_write_text(deferredwork.py:354) with adocumented rationale — a bare tmp+replace resets the file mode and turns a symlinked ledger into a
regular file. Its two sibling writers do not:
append_decision—deferredwork.py:389:path.write_text(text, encoding="utf-8")append_entry—deferredwork.py:492:path.write_text(text + sep + block, encoding="utf-8")Path.write_textopens with mode'w'(truncate) and then encodes. Any failure between those twopoints — an unencodable value,
ENOSPC,EIO— leaves a zero-byte ledger. Every deferred-workentry is gone.
Reproduced during review: an
append_decisioncall that raised mid-write left a 35-byte ledger at0 bytes, while the same value through
mark_doneleft it intact.Why the asymmetry matters
mark_done_many's docstring already argues the case for this exact file — "swapping a fresh inodeover the ledger otherwise resets its mode (a
0600ledger silently becoming world-readable) andturns a symlinked ledger into a regular file". That reasoning applies unchanged to the other two
writers; the protection just was not extended to them.
An asymmetric guard between siblings is itself the finding: a reader cannot tell whether the
difference is deliberate.
Fix shape
Route
append_decisionandappend_entrythroughatomic_write_texttoo. Noteappend_entrycreates the file when absent (
path.parent.mkdir(...)), so the helper needs to keep working for anot-yet-existing target.
Found during the #305 review.