///|
/// Rename metadata without permitting ambiguity or changing numerical values.
pub fn rename_curve(
  log : Log,
  name : String,
  replacement : String,
) -> Log raise WellError {
  validate(log)
  let column = curve_index(log, name)
  let old = log.curves[column]
  let curves = log.curves.copy()
  curves[column] = {
    name: replacement,
    unit: old.unit,
    description: old.description,
    api_code: old.api_code,
  }
  rebuild(log, curves, log.rows)
}

///|
/// Affine value transform; depth uses dedicated dimensional conversion instead.
pub fn affine_curve(
  log : Log,
  name : String,
  scale : Double,
  offset : Double,
) -> Log raise WellError {
  validate(log)
  if !finite(scale) || !finite(offset) {
    fail("affine coefficients must be finite")
  }
  let column = curve_index(log, name)
  if column == 0 {
    fail("use convert_curve for depth conversion")
  }
  let rows = log.rows.map(row => row.copy())
  for row in rows {
    if row[column] is Some(value) {
      let result = value * scale + offset
      if !finite(result) {
        fail("affine overflow")
      }
      row[column] = Some(result)
    }
  }
  rebuild(log, log.curves, rows)
}

///|
/// Null samples outside inclusive bounds rather than silently clamping outliers.
pub fn mask_range(
  log : Log,
  name : String,
  minimum : Double,
  maximum : Double,
) -> Log raise WellError {
  validate(log)
  if !finite(minimum) || !finite(maximum) || minimum > maximum {
    fail("invalid masking range")
  }
  let column = curve_index(log, name)
  if column == 0 {
    fail("depth cannot be masked")
  }
  let rows = log.rows.map(row => row.copy())
  for row in rows {
    if row[column] is Some(value) && (value < minimum || value > maximum) {
      row[column] = None
    }
  }
  rebuild(log, log.curves, rows)
}

///|
/// Add an explicitly described derived column, validating all input and output.
pub fn append_curve(
  log : Log,
  curve : Curve,
  values : Array[Double?],
) -> Log raise WellError {
  validate(log)
  if values.length() != log.rows.length() {
    fail("derived curve row count mismatch")
  }
  let curves = log.curves.copy()
  curves.push(curve)
  let rows = log.rows.map(row => row.copy())
  for i in 0..