manifest_value, manifest_has and manifest_foreign each read the file with a
bare `while read`, which stops at EOF without ever handing over a populated
partial line. An unterminated final line therefore read as ABSENT — and
absent is exactly the input both convergence rules key off, so the file's
last line was the one least able to survive the miss.
Three failures, in descending order of how much they cost:
- A file truncated mid-write ends AT bootstrapped_at, so the unreadable line
is the birth stamp itself. Rule 1 saw no at-stamp and regenerated the pair
as now() — overwriting the one field that can never be reconstructed. A
fixture born 2020-01-01 came back stamped with the current clock.
- A whole file whose last line is converged_at read as empty, so Rule 2's
one-time repair re-fired on EVERY run: the render stopped being a function
of (existing file, running version) and the clock reached the file after
all. This is the crux property of the feature, broken by a missing byte.
- manifest_foreign dropped an unterminated foreign line entirely, so the
writer ate a later command's provenance — the exact preservation contract
the function exists to keep.
The idiom is the repo's own: lib/users-config.sh:49 reads
`|| [ -n "$line" ]` for the same reason.
Reading the line correctly also repairs the file, since the rewritten copy
is newline-terminated — asserted as "the source plus the newline it was
missing, and nothing else", because a plain ends-in-newline check stays
green on an implementation that drops the final line.
7 tests, each observed RED against the unfixed reader.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>