At-least-once without tears
The rule is simple: advance the checkpoint only after the loader has durably written a batch. A crash replays the last batch; idempotent sinks make that a non-event.
extract -> transform -> load -> checkpoint
One invariant, applied everywhere, is worth more than a dozen special cases.