ADR 0219 — Journal damage is data, an IO failure is an error
Context
The writer already rotates an unfinished active file as a whole. But a NUL tail
kept in an archive breaks a consumer that calls JSON.parse on every line.
The partialTails counter describes only the current run and does not explain
archived damage. The cause of the NUL is unknown; flush() does not promise fsync.
Decision
One readDiagnosticJournal in the filesystem leaf reads finite snapshots with a
bounded line buffer. The shared frame schema and schema-backed results stay in the
pure application part. Valid frames pass the owner’s eventSchema; damaged lines
produce separate anomalies with the cause, file, byte offset/line and the size of
the skipped part. Access errors, an unsafe file type and truncation throw.
A valid frame without a trailing LF stays available together with a warning.
The startup writer uses the same reader under an exclusive lock. The check of
historical events is limited to the JSON/frame contract. The recovery status keeps
counts and the first/last anomaly; a repeated start restores it from the saved
files. A damaged active file is rotated as a whole with the usual finite retention.
maxFiles: 1 refuses before any data is deleted and releases the lock
(amended by ADR 0240: by default the journal quarantines such a file and starts;
the refusal is the onStartupRefusal: 'fail' policy).
Alternatives and verification
Silently ignoring invalid rows hides the loss of evidence. Separate consumer parsers split the rules in two. Truncating the tail destroys the original bytes; a new quarantine/sidecar registry adds its own lifecycle and storage. The shared reader and restorable diagnostics give a verifiable result without them.
Real-file tests check NUL in active/.1/.7, restart/append, byte preservation, oversized/interior rows, torn UTF-8/JSON, schema refusals, a real EACCES, a symlink, truncation and cleanup on cancellation. Recovery does not become durable replay and is not sent recursively into a damaged writer. The decision serves I3, I8, I10 and I13.