///|
/// Dirty means revision identity differs from saved, or a preview differs.
/// It does not prove a file was saved: call mark_saved only after successful I/O.
pub fn[T] History::is_dirty(self : History[T]) -> Bool {
  self.saved_id != Some(self.revision_id()) ||
  !(self.same)(self.live, self.revisions[self.cursor].value)
}

///|
pub fn[T] History::saved_revision_id(self : History[T]) -> Int? {
  self.saved_id
}

///|
pub fn[T] History::mark_saved(self : History[T]) -> Result[Unit, String] {
  if !self.transactions.is_empty() {
    return Err("cannot mark a preview as saved; commit or rollback first")
  }
  self.merge_allowed = false
  self.saved_id = Some(self.revision_id())
  Ok(())
}

///|
/// Disassociate history from any external saved state, e.g. an untitled document.
pub fn[T] History::forget_saved(self : History[T]) -> Unit {
  self.saved_id = None
}

///|
/// Load a new baseline, forgetting navigation and marking this revision clean.
pub fn[T] History::reset(
  self : History[T],
  initial : T,
  label? : String = "Loaded",
) -> Result[Unit, String] {
  if !self.transactions.is_empty() {
    return Err("cannot reset during a transaction")
  }
  if self.next_id == 2147483647 {
    return Err("revision identifier space exhausted")
  }
  let id = self.next_id
  self.next_id += 1
  self.revisions = [{ value: (self.copy)(initial), label, group: "", id, }]
  self.merge_allowed = false
  self.cursor = 0
  self.live = (self.copy)(initial)
  self.saved_id = Some(id)
  Ok(())
}