///|
pub fn make_plan(
  project : Project,
  cache : CacheSnapshot,
  target : String?,
) -> (Array[String], Array[Array[String]], Array[TaskPlan]) raise MoonforgeError {
  let order = task_order(project, target)
  let levels = task_levels(project, order)
  let plan_map : Map[String, TaskPlan] = Map([])
  let plans = []
  for name in order {
    let task = get_task(project, name)
    let command_hash = hash_string([task.cmd])
    let input_hash = fingerprint_paths(project.root, task.inputs)
    let dependency_runs = []
    for dep in task.deps {
      match plan_map.get(dep) {
        Some(dep_plan) => if dep_plan.should_run { dependency_runs.push(dep) }
        None => ()
      }
    }
    let reason = if task.is_effectively_phony() {
      PhonyTask
    } else {
      match cache.tasks.get(task.name) {
        None => FirstRun
        Some(_) if !outputs_exist(project.root, task) => OutputMissing
        Some(entry) if entry.command_hash != command_hash => CommandChanged
        Some(entry) if entry.input_hash != input_hash => InputsChanged
        Some(entry) if entry.output_hash !=
          fingerprint_paths(project.root, task.outputs) => OutputsChanged
        Some(_) if !dependency_runs.is_empty() =>
          DependencyReran(dependency_runs)
        Some(_) => UpToDate
      }
    }
    let plan = {
      task,
      reason,
      should_run: reason != UpToDate,
      command_hash,
      input_hash,
    }
    plan_map[name] = plan
    plans.push(plan)
  }
  (order, levels, plans)
}

///|
pub fn plan_for_task(plans : Array[TaskPlan], task_name : String) -> TaskPlan? {
  for plan in plans {
    if plan.task.name == task_name {
      return Some(plan)
    }
  }
  None
}