So I've done some more thinking about this, and I think I've come up with a solution. The contract would be as follows
IF
0 CHECKSEQUENCEVERIFY DROP
<alice pubkey>
ELSE
<lock date> CHECKLOCKTIMEVERIFY DROP
<bob pubkey>
ENDIF
CHECKSIG
If Alice wishes to spend before
lock date, she publishes a transaction with the input sequence number set as [0x0000, 0xffff]. Bob cannot double spend, as his transaction would not be accepted into the mempool.
If Bob wishes to spend after
lock date, he publishes a transaction with the appropriate nLockTime and the input sequence number set as 0xfffffffe, signaling that it is using locktime and is not replaceable.
If Alice publishes a transaction after
lock date, then Bob can publish his with a slightly higher fee, which will supersede Alice's due to the higher nSequence. The one potential weakness I can see is if Alice intentionally publishes a version with extremely high fees, essentially destroying some of the funds Bob is owed.