mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
15c872bfed
The pre-rollout dry run of op-log recovery (TASK-2198 U4) over the live DB's 175 candidates found one that would have been wiped. A load-test item's op-log of 90 tiny rows, with no base document, replays to an EMPTY document, and recovery would have written "" over its 1,285-byte body. A tab never does this: its lazy seed (TASK-1261) sees the empty fragment and seeds FROM items.content. MaterializeFlush now refuses a blank recovered body over a non-blank stored one, decided in the precheck against the row locked for the write, as the new outcome empty_document. Nothing is written: no body, version, seq or watermark. Recovery treats it as terminal until the op-log changes (exhaust, which now takes its kind), so the sweep does not replay it every minute, and it logs the one exhaustion WARN with last_error_kind=empty_document. Stated divergence: a tab refuses only when the fragment is EMPTY, so a user who truly cleared an item's text and then crashed would have flushed "". Recovery cannot see the fragment, so it keeps the old body there too, which is the conservative side. No shrink threshold, by ruling: a tab would flush a shorter body, and the dry run found no such case. Gate: the dry run re-run on this branch, flushing into a disposable COPY of the same snapshot. All 175 items produce the same recovered body and class as the first run. TASK-1270 is refused (empty_document) with its body intact; 156 are stamped, 17 applied, and 1 (a deadline failure) is never flushed. Legs: store (blank and whitespace-only, nothing written) and recovery (refused, terminal, one WARN, reset when the op-log grows). Mutants: a guard that never fires (2 red), no exhaust (1), wrong kind (1). Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC