///|
/// Build a deterministic measurement plan from features and devices.
pub fn plan(
  part_id~ : String,
  features : Array[Feature],
  devices : Array[Device],
) -> MeasurementPlan {
  let steps = []
  let warnings = []
  let mut capable = true
  for feature in features {
    match choose_device(feature~, devices~) {
      None => {
        warnings.push("no device for " + feature.id)
        capable = false
      }
      Some(candidate) => {
        let device = candidate.device
        let count = repeat_count(feature, device)
        let strategy = if feature.kind == Cylinder {
          Scan(count=if count < 5 { 5 } else { count })
        } else if count == 1 {
          SinglePoint
        } else {
          RepeatedPoint(count~)
        }
        let uncertainty = estimate_uncertainty(strategy, device)
        if !capability_ok(feature, uncertainty) {
          warnings.push("device capability is insufficient for " + feature.id)
          capable = false
        }
        steps.push({
          feature_id: feature.id,
          device_id: device.id,
          strategy,
          direction: if feature.datum {
            "datum-aligned"
          } else {
            "normal"
          },
          uncertainty,
        })
      }
    }
  }
  let coverage = if features.length() == 0 {
    1.0
  } else {
    steps.length().to_double() / features.length().to_double()
  }
  {
    part_id,
    steps,
    summary: { coverage, capability: capable, steps: steps.length(), warnings },
  }
}

///|
/// Find a matching observation and classify it.
pub fn verdict(
  plan : MeasurementPlan,
  feature : Feature,
  observations : Array[Observation],
) -> Verdict {
  ignore(plan)
  let mut observation = None
  for item in observations {
    if item.feature_id == feature.id && observation is None {
      observation = Some(item)
    }
  }
  classify(feature, observation)
}