/// FIST-Mbt engine: DAG 扩展模块。
/// 提供:critical_path(最长依赖链)、parallelism(当前可并行度)、dag_ascii(ASCII 依赖图)。
/// 仅依赖既有 FistEngine 能力(list_all / get_task / deps_satisfied),不改原有行为。

/// ==================== 内部工具 ====================

///|
/// 生成 n 层两空格缩进
fn dag_indent(n : Int) -> String {
  let mut s = ""
  for i = 0; i < n; i = i + 1 {
    s = s + "  "
  }
  s
}

///|
/// 用逗号拼接依赖 id 列表
fn dag_join_deps(deps : Array[String]) -> String {
  let mut s = ""
  for i = 0; i < deps.length(); i = i + 1 {
    if i > 0 {
      s = s + ","
    }
    s = s + deps[i]
  }
  s
}

/// ==================== 关键路径 ====================

///|
/// 求以 id 结尾的最长依赖链(沿 depends_on 边回溯;memo 化 DFS,含环保护)。
/// 返回序列按依赖先后排列(最底层依赖在前,id 自身在后)。
fn FistEngine::longest_chain_ending(
  self : FistEngine,
  id : String,
  memo : Map[String, Array[String]],
  visiting : Map[String, Bool],
) -> Array[String] {
  match memo.get(id) {
    Some(c) => return c
    None => ()
  }
  // 环保护:当前 DFS 路径上已出现过则直接返回自身,避免无限递归
  match visiting.get(id) {
    Some(_) => return [id]
    None => ()
  }
  visiting.set(id, true)
  let deps = match self.get_task(id) {
    Some(t) => t.depends_on
    None => []
  }
  let mut best : Array[String] = []
  for d in deps {
    let chain = self.longest_chain_ending(d, memo, visiting)
    if chain.length() > best.length() {
      best = chain
    }
  }
  visiting.remove(id)
  let out = best.copy()
  out.push(id)
  memo.set(id, out)
  out
}

///|
/// 关键路径:全图中按 depends_on 建图后的最长依赖链(任务 id 序列)。
/// 多条等长链取先遇到的一条;空仓库返回空数组。
pub fn FistEngine::critical_path(self : FistEngine) -> Array[String] {
  let memo : Map[String, Array[String]] = Map([])
  let visiting : Map[String, Bool] = Map([])
  let mut best : Array[String] = []
  for t in self.list_all() {
    let chain = self.longest_chain_ending(t.id, memo, visiting)
    if chain.length() > best.length() {
      best = chain
    }
  }
  best
}

/// ==================== 并行度 ====================

///|
/// 当前可并行执行的任务数:状态为待领取且依赖已满足的任务数。
pub fn FistEngine::parallelism(self : FistEngine) -> Int {
  let mut n = 0
  for t in self.list_all() {
    if t.get_status().is_pending() && self.deps_satisfied(t.get_id()) {
      n = n + 1
    }
  }
  n
}

/// ==================== ASCII 依赖图 ====================

///|
/// 计算任务在 DAG 中的层级(缩进深度):
/// 取「parent_id 父链深度」与「depends_on 依赖链深度」的较大者。
/// 父/依赖不在本命名空间内则不计入;环保护下返回 0。
fn FistEngine::dag_level(
  self : FistEngine,
  id : String,
  members : Map[String, Bool],
  memo : Map[String, Int],
  visiting : Map[String, Bool],
) -> Int {
  match memo.get(id) {
    Some(l) => return l
    None => ()
  }
  match visiting.get(id) {
    Some(_) => return 0
    None => ()
  }
  visiting.set(id, true)
  let mut level = 0
  match self.get_task(id) {
    Some(t) => {
      // 子任务按 parent_id 归组:父任务存在则至少缩进一层
      match t.parent_id {
        Some(p) =>
          if members.contains(p) && p != id {
            let pl = self.dag_level(p, members, memo, visiting)
            if pl + 1 > level {
              level = pl + 1
            }
          }
        None => ()
      }
      // depends_on 同样加深层级(依赖必须先完成,视觉上位于上层)
      for d in t.depends_on {
        if d != id && members.contains(d) {
          let dl = self.dag_level(d, members, memo, visiting)
          if dl + 1 > level {
            level = dl + 1
          }
        }
      }
    }
    None => ()
  }
  visiting.remove(id)
  memo.set(id, level)
  level
}

///|
/// ASCII 树/缩进图:展示某命名空间下的任务依赖结构。
/// 层级由 parent_id(子任务归组)与 depends_on(依赖深度)共同决定,
/// 同层任务按 id 字典序稳定输出;跨命名空间的父/依赖不计入缩进。
/// 每行格式:<缩进> [<状态>](有依赖时追加 " (依赖: a,b)"),行尾换行。
pub fn FistEngine::dag_ascii(self : FistEngine, ns~ : String) -> String {
  let target = if ns == "" { "default" } else { ns }
  let tasks = self.store.list_tasks_in(target)
  let members : Map[String, Bool] = Map([])
  for t in tasks {
    members.set(t.id, true)
  }
  let memo : Map[String, Int] = Map([])
  let visiting : Map[String, Bool] = Map([])
  let levels : Map[String, Int] = Map([])
  for t in tasks {
    levels.set(t.id, self.dag_level(t.id, members, memo, visiting))
  }
  let sorted = tasks.copy()
  sorted.sort_by(fn(a, b) {
    let la = match levels.get(a.id) {
      Some(l) => l
      None => 0
    }
    let lb = match levels.get(b.id) {
      Some(l) => l
      None => 0
    }
    if la != lb {
      la - lb
    } else {
      a.id.compare(b.id)
    }
  })
  let mut out = ""
  for t in sorted {
    let lvl = match levels.get(t.id) {
      Some(l) => l
      None => 0
    }
    let mut line = dag_indent(lvl) + t.id + " [" + t.status_to_string() + "]"
    if t.depends_on.length() > 0 {
      line = line + " (依赖: " + dag_join_deps(t.depends_on) + ")"
    }
    out = out + line + "\n"
  }
  out
}

///|
/// 显式给任务追加一条前置依赖(构建 DAG 依赖边,不只靠 plan_deep 隐式父子)。
/// 校验:task 与 dep 均存在、非同一任务、且不会形成直接自环;去重追加并落库。
/// 返回追加后的任务 JSON。失败返回 Err(含人语原因)。
pub fn FistEngine::dag_depend(
  self : FistEngine,
  task_id~ : String,
  dep_id~ : String,
  now~ : String,
) -> Result[@core.Task, String] {
  let task = match self.store.get_task(task_id) {
    None => return Err("task not found: \{task_id}")
    Some(t) => t
  }
  if dep_id == task_id {
    return Err("不能依赖自身: \{task_id}")
  }
  let _dep = match self.store.get_task(dep_id) {
    None => return Err("依赖任务 \{dep_id} 不存在")
    Some(d) => d
  }
  // 已存在则幂等返回现任务
  if task.depends_on.contains(dep_id) {
    return Ok(task)
  }
  let c = task.with_depends_on(dep_id, now)
  match self.store.update_task(c) {
    Ok(_) => Ok(c)
    Err(e) => Err("依赖落库失败: \{e}")
  }
}

/// ==================== 瓶颈与松弛分析(PERT/CPM,R71) ====================

///|
/// 拓扑序(依赖先于被依赖;带环保护——成环任务归入 cycle,不阻塞计算)。
fn FistEngine::dag_topo_order(
  self : FistEngine,
) -> (Array[String], Array[String]) {
  let ids : Array[String] = []
  for t in self.list_all() {
    ids.push(t.get_id())
  }
  let indeg : Map[String, Int] = Map([])
  let deps_of : Map[String, Array[String]] = Map([])
  for id in ids {
    indeg.set(id, 0)
    deps_of.set(id, [])
  }
  // 建前置→后继边:任务 t 依赖 d(d 是 t 的前置)→ 边 d→t;indeg[t]++(t 的前置数)
  for t in self.list_all() {
    for d in t.depends_on {
      deps_of.set(d, deps_of.get(d).unwrap_or([]) + [t.get_id()])
      let cur = indeg.get(t.get_id()).unwrap_or(0)
      indeg.set(t.get_id(), cur + 1)
    }
  }
  let order : Array[String] = []
  let mut ready_ids : Array[String] = ids.filter(fn(id) {
    indeg.get(id).unwrap_or(0) == 0
  })
  while ready_ids.length() > 0 {
    let cur = ready_ids[0]
    // 移除队首(保持顺序)
    let rest : Array[String] = []
    for i = 1; i < ready_ids.length(); i = i + 1 {
      rest.push(ready_ids[i])
    }
    ready_ids = rest
    order.push(cur)
    // cur 的后继(以 cur 为前置的任务):前置完成 -> 后继入度-1
    for nxt in deps_of.get(cur).unwrap_or([]) {
      let nd = indeg.get(nxt).unwrap_or(0)
      indeg.set(nxt, nd - 1)
      if indeg.get(nxt).unwrap_or(0) == 0 {
        ready_ids = ready_ids + [nxt]
      }
    }
  }
  let cycle = ids.filter(fn(id) { indeg.get(id).unwrap_or(0) > 0 })
  (order, cycle)
}

///|
/// 瓶颈与松弛分析(PERT/CPM 硬核调度能力,R71):对每个任务计算
///   earliest(依赖全部就绪的最早开始步数)、latest(不拖慢总工期的最晚开始步数)、
///   slack(latest-earliest;0=关键路径上,>0=可灵活并行安排)。
/// 返回 { critical:[关键路径任务id], slack_map:{id:{earliest,latest,slack}}, makespan, cycle:[成环任务] }。
/// ——"找出谁在关键路径上、谁有松弛可并行",直接支撑排程优化(不是视图叠加)。
pub fn FistEngine::dag_slack_analysis(self : FistEngine) -> Json {
  let (order, cycle) = self.dag_topo_order()
  let ids : Array[String] = []
  for t in self.list_all() {
    ids.push(t.get_id())
  }
  // earliest: 前向——依赖全就绪的最早开始
  let earliest : Map[String, Int] = Map([])
  for id in ids {
    earliest.set(id, 0)
  }
  for id in order {
    match self.get_task(id) {
      Some(t) =>
        for d in t.depends_on {
          let de = earliest.get(d).unwrap_or(0)
          let ce = earliest.get(id).unwrap_or(0)
          if de + 1 > ce {
            earliest.set(id, de + 1)
          }
        }
      None => ()
    }
  }
  // makespan: 全图最长 earliest(总工期)
  let mut makespan = 0
  for id in ids {
    let e = earliest.get(id).unwrap_or(0)
    if e > makespan {
      makespan = e
    }
  }
  // latest: 反向——不拖慢总工期的最晚开始
  let latest : Map[String, Int] = Map([])
  for id in ids {
    latest.set(id, makespan)
  }
  let rev : Map[String, Array[String]] = Map([])
  for id in ids {
    rev.set(id, [])
  }
  for t in self.list_all() {
    for d in t.depends_on {
      rev.set(d, rev.get(d).unwrap_or([]) + [t.get_id()])
    }
  }
  for i = order.length() - 1; i >= 0; i = i - 1 {
    let id = order[i]
    let mut lt = makespan
    for nxt in rev.get(id).unwrap_or([]) {
      let ln = latest.get(nxt).unwrap_or(0)
      if ln - 1 < lt {
        lt = ln - 1
      }
    }
    if lt < 0 {
      lt = 0
    }
    latest.set(id, lt)
  }
  // slack = latest - earliest;0 → 关键路径
  let slack_map : Map[String, Json] = Map([])
  let critical : Array[String] = []
  for id in ids {
    let e = earliest.get(id).unwrap_or(0)
    let l = latest.get(id).unwrap_or(0)
    let slack = l - e
    if slack == 0 {
      critical.push(id)
    }
    slack_map.set(
      id,
      Json::object({
        "earliest": Json::number(e.to_double()),
        "latest": Json::number(l.to_double()),
        "slack": Json::number(slack.to_double()),
      }),
    )
  }
  critical.sort()
  Json::object({
    "makespan": Json::number(makespan.to_double()),
    "critical": Json::array(critical.map(fn(s) { Json::string(s) })),
    "slack_map": Json::object(slack_map),
    "cycle": Json::array(cycle.map(fn(s) { Json::string(s) })),
  })
}

/// ==================== 排程视图(R74,基于 slack 落成可执行排程) ====================

///|
/// 排程视图(R75 增强):在 dag_slack_analysis 基础上把任务落成可执行排程——
/// 按 slack 分两层批:critical 批(slack=0,关键路径瓶颈,须串行盯紧)与
/// flexible 批(slack>0,可按最早开始排序并行的弹性任务),每批附 assignee 当前占用,
/// 并对 flexible 批中未认领任务给出「负载感知建议派单」(suggest:活跃负载最低的已注册执行者)。
/// 复用 dag_slack_analysis(不重复算图)。——"谁在瓶颈、谁可并行派单"一目了然,排程优化闭环。
pub fn FistEngine::dag_schedule(self : FistEngine) -> Json {
  let slack = self.dag_slack_analysis()
  let ids : Array[String] = []
  for t in self.list_all() {
    ids.push(t.get_id())
  }
  // 负载感知:统计各已注册执行者名下活跃(未完成未归档)任务数
  let reg_names : Array[String] = []
  for (name, _abl, _created) in self.executor_list() {
    reg_names.push(name)
  }
  let load : Map[String, Int] = Map([])
  for n in reg_names {
    load.set(n, 0)
  }
  for t in self.list_all() {
    match t.assignee {
      Some(a) =>
        if load.contains(a) {
          let done = t.get_status().is_completed() ||
            t.get_status().to_string() == "已归档"
          if not(done) {
            load.set(a, load.get(a).unwrap_or(0) + 1)
          }
        }
      None => ()
    }
  }
  // 从 slack.slack_map 取每个任务的 slack 与 earliest
  let critical_batch : Array[Json] = []
  let flexible_batch : Array[Json] = []
  for id in ids {
    let mut sl = -1
    let mut ea = -1
    match slack.value("slack_map") {
      Some(Object(sm)) =>
        match sm.get(id) {
          Some(Object(o)) => {
            sl = match o.get("slack") {
              Some(Number(n, ..)) => n.to_int()
              _ => -1
            }
            ea = match o.get("earliest") {
              Some(Number(n, ..)) => n.to_int()
              _ => -1
            }
          }
          _ => ()
        }
      _ => ()
    }
    let assignee = match self.get_task(id) {
      Some(t) =>
        match t.assignee {
          Some(a) => a
          None => ""
        }
      None => ""
    }
    let row = Json::object({
      "task_id": Json::string(id),
      "earliest": Json::number(ea.to_double()),
      "slack": Json::number(sl.to_double()),
      "assignee": Json::string(assignee),
    })
    if sl == 0 {
      critical_batch.push(row)
    } else if sl > 0 {
      flexible_batch.push(row)
    }
  }
  // 弹性批按最早开始排序(可并行窗口)
  flexible_batch.sort_by(fn(a : Json, b : Json) -> Int {
    let ae = match a.value("earliest") {
      Some(Number(n, ..)) => n
      _ => 0.0
    }
    let be = match b.value("earliest") {
      Some(Number(n, ..)) => n
      _ => 0.0
    }
    if ae < be {
      -1
    } else if ae > be {
      1
    } else {
      0
    }
  })
  // R75 负载感知:flexible 批中未认领任务建议派给活跃负载最低的已注册执行者
  let flexible_adv : Array[Json] = flexible_batch.map(fn(row) {
    let asg = match row.value("assignee") {
      Some(Json::String(s)) => s
      _ => ""
    }
    if asg != "" {
      return row
    }
    // 已注册执行者中选负载最低者(无注册则不建议)
    let mut best = ""
    let mut best_load = 2147483647
    for n in reg_names {
      let l = load.get(n).unwrap_or(0)
      if l < best_load {
        best_load = l
        best = n
      }
    }
    let m = match row {
      Object(o) => o
      _ => Map([])
    }
    m.set("suggest", Json::string(if best != "" { best } else { "" }))
    Json::object(m)
  })
  Json::object({
    "basis": Json::string("PERT/CPM slack(基于 dag_slack_analysis)"),
    "critical_batch": Json::array(critical_batch),
    "flexible_batch": Json::array(flexible_adv),
    "note": Json::string(
      "critical_batch=关键路径瓶颈(须串行盯紧);flexible_batch=有松弛可按最早开始并行的任务,未认领项附 suggest=活跃负载最低的已注册执行者(R75 负载感知)",
    ),
  })
}

/// ==================== 成本路由(R77,STAR 式依赖图成本路由) ====================

///|
/// 依赖图成本路由(R77,STAR 蒸馏简化版):在 dag_schedule 排程视图之上补「成本」维度——
/// 对 flexible(slack>0)批中未认领任务,按拓扑序贪心给出建议执行者:在「能力覆盖的
/// 已注册执行者」里选 执行成本 + 切换成本 之和最小者。
///   - 执行成本:难度档映射(易/中/难 → 1/2/3,复用 extract_difficulty 单一来源);
///   - 能力约束:任务所需能力 = 描述命中的最长已注册能力标签(复用 auto_want),
///     未命中标签的执行者被排除(want 空则全命中);
///   - 切换成本:任务依赖链上已路由执行者若与候选不同则 +1(STAR 的上下文切换税);
///   - 成本相等时取活跃负载最低者(与 R75 一致)。
/// 纯计算、无副作用(只给建议不 claim)。——排程优化闭环最终形态:
/// critical_path→slack→schedule→suggest→cost_route。
pub fn FistEngine::dag_cost_route(self : FistEngine) -> Json {
  let slack = self.dag_slack_analysis()
  let (order, _cycle) = self.dag_topo_order()
  // 执行者注册表:候选顺序按注册顺序(executor_list 稳定)
  let reg_names : Array[String] = []
  let reg_abl : Map[String, Array[String]] = Map([])
  for (name, abl, _created) in self.executor_list() {
    reg_names.push(name)
    reg_abl.set(name, parse_abilities(abl))
  }
  let load = self.executor_loads(reg_names)
  // 已路由执行者记录(切换成本基准):已认领任务先登记为固定占用
  let routed : Map[String, String] = Map([])
  for t in self.list_all() {
    match t.assignee {
      Some(a) => routed.set(t.get_id(), a)
      None => ()
    }
  }
  let rows : Array[Json] = []
  let mut total_cost = 0
  for id in order {
    // 只处理 slack>0(flexible)且未认领的任务
    let mut sl = -1
    match slack.value("slack_map") {
      Some(Object(sm)) =>
        match sm.get(id) {
          Some(Object(o)) =>
            match o.get("slack") {
              Some(Number(n, ..)) => sl = n.to_int()
              _ => ()
            }
          _ => ()
        }
      _ => ()
    }
    if sl <= 0 {
      continue
    }
    if routed.contains(id) {
      continue
    }
    let desc = match self.get_task(id) {
      Some(t) => t.description
      None => ""
    }
    if desc == "" {
      continue
    }
    // 任务所需能力:描述命中的最长已注册能力标签(无注册标签则空 = 全命中)
    let want = self.auto_want(desc)
    // 难度 → 执行成本(易/中/难 → 1/2/3,无难度标记按 1)
    let lab = triage_label_of(desc)
    let est = if lab == "难" { 3 } else if lab == "中" { 2 } else { 1 }
    // 依赖任务的执行者(切换成本基准)
    let deps_asg : Array[String] = []
    match self.get_task(id) {
      Some(t) =>
        for d in t.depends_on {
          match routed.get(d) {
            Some(a) => deps_asg.push(a)
            None => ()
          }
        }
      None => ()
    }
    // 候选执行者:能力覆盖 want(want 空 → 全部);选 执行成本+切换成本 最小,相等取负载最低
    let mut best = ""
    let mut best_total = 2147483647
    let mut best_switch = 0
    for n in reg_names {
      let abl = reg_abl.get(n).unwrap_or([])
      if want != "" && not(abl.contains(want)) {
        continue
      }
      let sw = if deps_asg.length() > 0 && not(deps_asg.contains(n)) {
        1
      } else {
        0
      }
      let ttl = est + sw
      if ttl < best_total {
        best_total = ttl
        best = n
        best_switch = sw
      } else if ttl == best_total {
        let bl = load.get(best).unwrap_or(0)
        let nl = load.get(n).unwrap_or(0)
        if nl < bl {
          best = n
          best_switch = sw
        }
      }
    }
    if best == "" {
      continue
    }
    routed.set(id, best)
    total_cost = total_cost + best_total
    rows.push(
      Json::object({
        "task_id": Json::string(id),
        "assignee": Json::string(best),
        "est_cost": Json::number(est.to_double()),
        "switch_cost": Json::number(best_switch.to_double()),
        "total": Json::number(best_total.to_double()),
      }),
    )
  }
  Json::object({
    "basis": Json::string(
      "STAR 式依赖图成本路由:执行成本(难度档易/中/难→1/2/3)+切换税(依赖执行者不同则+1),能力约束过滤,拓扑贪心",
    ),
    "cost_route": Json::array(rows),
    "total_est_cost": Json::number(total_cost.to_double()),
    "note": Json::string(
      "仅对 slack>0 未认领任务给出建议执行者(纯读不 claim);无注册执行者或能力不匹配项跳过",
    ),
  })
}

/// ==================== 预算按依赖图阶段切分(R81,ZEBRA 水填充蒸馏简化版) ====================

///|
/// 预算切分的取数范围(BUG-20①):root 给定时取该任务子树,否则按 ns(空=default→全库按 ns="")。
fn FistEngine::budget_scope_ids(
  self : FistEngine,
  root : String,
  ns : String,
) -> Array[String] {
  if root != "" {
    self.subtree_ids(root)
  } else {
    let pool = if ns == "" { self.list_all() } else { self.list_in_ns(ns) }
    let out : Array[String] = []
    for t in pool {
      out.push(t.get_id())
    }
    out
  }
}

///|
/// 作用域自证字段(BUG-20③):这次切分覆盖的是哪些任务,写在返回体里而不是靠调用方回忆。
fn budget_scope_json(root : String, ns : String, count : Int) -> Json {
  Json::object({
    "root_task_id": Json::string(root),
    "namespace": Json::string(ns),
    "scoped_task_count": Json::number(count.to_double()),
    "scope_kind": Json::string(
      if root != "" {
        "subtree"
      } else if ns != "" {
        "namespace"
      } else {
        "whole-store"
      },
    ),
  })
}

///|
/// 预算按依赖图阶段切分(R81,ZEBRA 背包水填充蒸馏简化版):给定总预算,
/// 按任务 DAG 阶段(slack 分析的 earliest 层级 0..makespan)切分——每阶段基础份额 =
/// 该阶段任务数 × 难度权重(难3/中2/易1,复用难度档单一来源;无难度标记按 1),
/// 总权重归一化后乘总预算(向下取整,余数补给最大权重阶段)。
/// 返回 { verdict, stages:[{level, tasks, difficulty_sum, share}], total_budget, makespan, scope, note }。
/// ——"预算在依赖图上按阶段切分:瓶颈阶段(关键路径层)占额可见,超支先预警",机制设计叙事。
///
/// BUG-20 改的三件事:
/// ① 过去**只吃 budget**,task_id/namespace 传了也不生效,算的永远是整个 store(实测
///    某轮 T0r292 子树 7 个任务,它报 tasks=1152=全库数)——现在 root/ns 真的参与取数,
///    与同族 dag_slack/dag_mc 的作用域口径一致;
/// ② budget<=0(含负数)过去直接产出负份额(-50 → -42/-5/-2),既不拒也不警——现在拒绝;
/// ③ 切了哪些任务写进返回体的 `scope` 自证。root/ns 都不给=全库(旧行为,零回归),
///    但这一点从此写在返回体里,而不是靠调用方记得自己没传。
pub fn FistEngine::budget_split_by_dag(
  self : FistEngine,
  budget : Int,
  root? : String = "",
  ns? : String = "",
) -> Json {
  if budget <= 0 {
    return Json::object({
      "stages": Json::array([]),
      "total_budget": Json::number(budget.to_double()),
      "verdict": Json::string("rejected"),
      "note": Json::string(
        "budget 必须是正整数(收到 " +
        budget.to_string() +
        "):负预算只会切出负份额,那不是预算而是赤字",
      ),
    })
  }
  let slack = self.dag_slack_analysis()
  // 每任务 earliest 作为阶段
  let level_of : Map[String, Int] = Map([])
  match slack.value("slack_map") {
    Some(Object(sm)) =>
      for id in sm.keys() {
        match sm.get(id) {
          Some(Object(o)) =>
            match o.get("earliest") {
              Some(Number(n, ..)) => level_of.set(id, n.to_int())
              _ => ()
            }
          _ => ()
        }
      }
    _ => ()
  }
  // ---- 作用域取数(BUG-20①)----
  let scope_ids = self.budget_scope_ids(root, ns)
  let root_missing = match self.get_task(root) {
    None => true
    Some(_) => false
  }
  if root != "" && root_missing {
    return Json::object({
      "stages": Json::array([]),
      "total_budget": Json::number(budget.to_double()),
      "verdict": Json::string("insufficient"),
      "scope": budget_scope_json(root, ns, 0),
      "note": Json::string(
        "作用域内无任务:根任务不存在 " +
        root +
        "(与 dag_mc 同口径——不退回全库,退回全库正是本条缺陷的形状)",
      ),
    })
  }
  // 阶段聚合:难度权重(难3/中2/易1,无难度 1)+ 任务数
  let stage_weight : Map[Int, Int] = Map([])
  let stage_count : Map[Int, Int] = Map([])
  let in_scope : Map[String, Bool] = Map([])
  for id in scope_ids {
    in_scope.set(id, true)
  }
  for t in self.list_all() {
    if not(in_scope.get(t.get_id()).unwrap_or(false)) {
      continue
    }
    let lv = level_of.get(t.get_id()).unwrap_or(0)
    let lab = triage_label_of(t.description)
    let w = if lab == "难" { 3 } else if lab == "中" { 2 } else { 1 }
    stage_weight.set(lv, stage_weight.get(lv).unwrap_or(0) + w)
    stage_count.set(lv, stage_count.get(lv).unwrap_or(0) + 1)
  }
  let mut total_w = 0
  let mut max_lv = 0
  let mut max_w = 0
  for lv in stage_weight.keys() {
    let w = stage_weight.get(lv).unwrap_or(0)
    total_w = total_w + w
    if w > max_w {
      max_w = w
      max_lv = lv
    }
  }
  // 归一化切分:向下取整,余数补给最大权重阶段
  let mut used = 0
  let shares : Map[Int, Int] = Map([])
  for lv in stage_weight.keys() {
    let w = stage_weight.get(lv).unwrap_or(0)
    let share = if total_w <= 0 { 0 } else { budget * w / total_w }
    shares.set(lv, share)
    used = used + share
  }
  if total_w > 0 && used < budget {
    let cur = shares.get(max_lv).unwrap_or(0)
    shares.set(max_lv, cur + (budget - used))
  }
  // 输出阶段行(按层级升序)
  let stages : Array[Json] = []
  let lvs : Array[Int] = stage_weight.keys().to_array()
  lvs.sort()
  for lv in lvs {
    stages.push(
      Json::object({
        "level": Json::number(lv.to_double()),
        "tasks": Json::number(stage_count.get(lv).unwrap_or(0).to_double()),
        "difficulty_sum": Json::number(
          stage_weight.get(lv).unwrap_or(0).to_double(),
        ),
        "share": Json::number(shares.get(lv).unwrap_or(0).to_double()),
      }),
    )
  }
  let mut makespan = 0
  match slack.value("makespan") {
    Some(Number(n, ..)) => makespan = n.to_int()
    _ => ()
  }
  if scope_ids.is_empty() {
    // 空作用域不出"三阶段全 0"的像模像样的切分(BUG-20①的另一半)
    return Json::object({
      "stages": Json::array([]),
      "total_budget": Json::number(budget.to_double()),
      "makespan": Json::number(0.0),
      "verdict": Json::string("insufficient"),
      "scope": budget_scope_json(root, ns, 0),
      "note": Json::string(
        "作用域内无任务,未做切分(不是预算问题,是取数范围为空)",
      ),
    })
  }
  Json::object({
    "stages": Json::array(stages),
    "total_budget": Json::number(budget.to_double()),
    "makespan": Json::number(makespan.to_double()),
    "verdict": Json::string("ok"),
    "scope": budget_scope_json(root, ns, scope_ids.length()),
    "note": Json::string(
      "阶段=slack 分析的 earliest 层级;份额=阶段难度权重占总量比例×总预算(余数补最大权重阶段)——瓶颈阶段占额可见,超支先预警",
    ),
  })
}

/// ==================== 进度预算路由门控(R88,PROGROUTER 蒸馏) ====================

///|
/// 收集 task_id 的子树 id(自身 + 全部 parent_id 后代,BFS;复用 children_of)。
fn FistEngine::subtree_ids(self : FistEngine, root : String) -> Array[String] {
  let out : Array[String] = [root]
  let mut i = 0
  while i < out.length() {
    let pid = out[i]
    for c in self.children_of(pid) {
      out.push(c.get_id())
    }
    i = i + 1
  }
  out
}

///|
/// 进度预算路由门控(R88,PROGROUTER arXiv 2608.25992 蒸馏简化版):在线进度引导——
/// 对任务子树按「已消耗预算(难度权重 易/中/难→1/2/3,复用 extract_difficulty 单一来源)
/// 与 完成进度(已完成+已归档 / 子树任务数)」做双路径剩余成本预测:
///   线性 = 燃尽率(burn_rate=spent/progress) × 剩余工作量(1-progress);
///   保守 = 线性 × 1.2(PROGROUTER 双路径,缓冲 20%)。
/// 元门控给决策:OK(预算充足继续)/ CAUTION(线性可行但缓冲不足 → 建议降档/缩范围)/
/// ESCALATE(线性已超支 → 建议追加预算或暂停)。纯计算、无副作用。
/// spent_override>0 可覆盖自动估算(如按真实 token/成本统计注入);0 或缺省=自动。
pub fn FistEngine::progress_gate(
  self : FistEngine,
  task_id : String,
  budget : Int,
  spent_override : Int,
) -> Json {
  match self.get_task(task_id) {
    None =>
      return Json::object({
        "ok": Json::boolean(false),
        "task_id": Json::string(task_id),
        "note": Json::string("任务不存在: " + task_id),
      })
    Some(_) => ()
  }
  if budget <= 0 {
    return Json::object({
      "ok": Json::boolean(false),
      "task_id": Json::string(task_id),
      "budget": Json::number(0.0),
      "note": Json::string("budget 必须为正"),
    })
  }
  let ids = self.subtree_ids(task_id)
  let mut total = 0
  let mut done = 0
  let mut started_weight = 0
  for id in ids {
    match self.get_task(id) {
      Some(x) => {
        total = total + 1
        let st = x.get_status()
        if st.is_completed() || st.is_archived() {
          done = done + 1
        }
        // 已开工(非待领取)的任务计入已消耗预算(难度权重)
        if not(st.is_pending()) {
          let lab = triage_label_of(x.get_description())
          let w = if lab == "难" { 3 } else if lab == "中" { 2 } else { 1 }
          started_weight = started_weight + w
        }
      }
      None => ()
    }
  }
  let progress = if total <= 0 {
    0.0
  } else {
    done.to_double() / total.to_double()
  }
  let spent = if spent_override > 0 { spent_override } else { started_weight }
  let remaining = (budget - spent).to_double()
  // 燃尽率:每单位进度消耗的预算(未开工时为 0)
  let burn = if progress > 0.0 { spent.to_double() / progress } else { 0.0 }
  let remaining_work = 1.0 - progress
  let linear = burn * remaining_work
  let conservative = linear * 1.2
  let (verdict, action) = if progress >= 1.0 {
    (
      "OK",
      "已完成,预算结余 " +
      (budget - spent).to_string() +
      "(负为超支)",
    )
  } else if linear <= remaining && conservative <= remaining {
    ("OK", "继续推进(按当前燃尽率预算充足)")
  } else if linear <= remaining {
    (
      "CAUTION", "缩减范围/降档(如选更简单变体或减少拆分数)后继续",
    )
  } else {
    ("ESCALATE", "追加预算或暂停复核(按当前燃尽率将超支)")
  }
  Json::object({
    "ok": Json::boolean(true),
    "task_id": Json::string(task_id),
    "budget": Json::number(budget.to_double()),
    "spent": Json::number(spent.to_double()),
    "remaining": Json::number(remaining),
    "progress": Json::number(progress),
    "burn_rate": Json::number(burn),
    "remaining_work": Json::number(remaining_work),
    "projection": Json::object({
      "linear": Json::number(linear),
      "conservative": Json::number(conservative),
    }),
    "verdict": Json::string(verdict),
    "action": Json::string(action),
    "basis": Json::string(
      "进度预算路由门控(PROGROUTER arXiv2608.25992 蒸馏):双路径预测 线性=燃尽率×剩余工作量 / 保守=1.2×线性; 元门控 OK/CAUTION/ESCALATE(R88)",
    ),
  })
}

///|
/// 反馈驱动的计划修订(R98,ReAct arXiv 2210.03629 / CoPAL arXiv 2310.07263 蒸馏落地):
/// 把"拆完即弃"升级为"执行中持续修订"——给定根任务及子任务执行反馈,计算计划三分:
/// keep(已证有效、承诺保留)/ rework(失败或其依赖链受牵连,需返工/重做)/ ready
/// (依赖全部有效且未执行 = 下一步可做)。feedback 缺省时读真实状态(已完成/已归档
/// =ok;已打回/已暂停=否;其余=未证)。级联:失败任务的下游依赖者一并 rework
/// (plan commitment 控涟漪——与 saga_repair 依赖者闭包同构)。纯计算、只读不写库
/// (决策建议,执行权在 agent/指挥官,与 saga 返回补偿序列同一抽象层)。
pub fn FistEngine::plan_revise(
  self : FistEngine,
  root_task_id : String,
  feedback? : Array[Json] = [],
  ns? : String = "default",
) -> Json {
  if self.get_task(root_task_id) is None {
    return Json::object({
      "ok": Json::boolean(false),
      "root_task_id": Json::string(root_task_id),
      "note": Json::string("根任务不存在: " + root_task_id),
    })
  }
  let sub = self.subtree_ids(root_task_id)
  // 显式反馈表:task_id -> ok(覆盖读状态)
  let fb : Map[String, Bool] = Map([])
  for f in feedback {
    let tid_opt = match f.value("task_id") {
      Some(Json::String(s)) => Some(s)
      _ => None
    }
    let ok_opt = match f.value("ok") {
      Some(j) => j.as_bool()
      _ => None
    }
    match (tid_opt, ok_opt) {
      (Some(tid), Some(okv)) => fb.set(tid, okv)
      _ => ()
    }
  }
  let basis = if feedback.is_empty() { "status" } else { "feedback" }
  // 证据判定:ok / no / ?
  let ev : Map[String, String] = Map([])
  for id in sub {
    match self.get_task(id) {
      None => ()
      Some(t) => {
        let v = match fb.get(id) {
          Some(b) => if b { "ok" } else { "no" }
          None => {
            let st = t.get_status()
            if st.is_completed() || st.is_archived() {
              "ok"
            } else if st.is_rejected() || st.is_paused() {
              "no"
            } else {
              "?"
            }
          }
        }
        ev.set(id, v)
      }
    }
  }
  // 类别:keep / rework / ready(依赖先分类,迭代至稳定;根任务是计划容器,不入桶)
  let cat : Map[String, String] = Map([])
  let mut changed = true
  while changed {
    changed = false
    for id in sub {
      if id == root_task_id {
        continue
      }
      if cat.get(id) is Some(_) {
        continue
      }
      match self.get_task(id) {
        None => ()
        Some(t) => {
          let mut dep_rework = false
          let mut dep_undone = false
          for d in t.depends_on {
            if sub.contains(d) {
              match cat.get(d) {
                Some("rework") => dep_rework = true
                Some("keep") => ()
                _ => dep_undone = true
              }
            } else {
              // 子树外依赖:按真实状态判定是否完成
              match self.get_task(d) {
                Some(dt) =>
                  if not(dt.get_status().is_completed()) {
                    dep_undone = true
                  }
                None => dep_undone = true
              }
            }
          }
          let tag = match ev.get(id) {
            Some("no") => "rework"
            Some("ok") => if dep_rework { "rework" } else { "keep" }
            _ =>
              if dep_rework {
                "rework"
              } else if dep_undone {
                ""
              } else {
                "ready"
              }
          }
          if tag != "" {
            cat.set(id, tag)
            changed = true
          }
        }
      }
    }
  }
  let keep : Array[String] = []
  let rework : Array[String] = []
  let ready : Array[String] = []
  for id in sub {
    match cat.get(id) {
      Some("keep") => keep.push(id)
      Some("rework") => rework.push(id)
      Some("ready") => ready.push(id)
      _ => ()
    }
  }
  Json::object({
    "ok": Json::boolean(true),
    "root_task_id": Json::string(root_task_id),
    "basis": Json::string(basis),
    "keep_count": Json::number(keep.length().to_double()),
    "rework_count": Json::number(rework.length().to_double()),
    "ready_count": Json::number(ready.length().to_double()),
    "keep": Json::array(keep.map(fn(s) { Json::string(s) })),
    "rework": Json::array(rework.map(fn(s) { Json::string(s) })),
    "ready": Json::array(ready.map(fn(s) { Json::string(s) })),
    "note": Json::string(
      "反馈驱动的计划修订(ReAct 交错 + CoPAL 分级纠正蒸馏):keep=已证有效承诺保留 / rework=失败或其依赖链受牵连需返工(控涟漪)/ ready=依赖全部有效且未执行,下一步可做。",
    ),
  })
}

///|
/// 全局目标校验(R100,goal drift arXiv 2505.02709 / Repetitiveness Rate 2603.12710 /
/// IntentCUA 2602.17049 / HiMAP ICML2026 蒸馏落地)——**advisory 判据,不是硬门**(BUG-25)。
///
/// 词法口径(单真源 @evolve.tokens):similarity = max(jaccard, coverage),drift = 1 - similarity。
///   · coverage 非对称(|根∩子|/|根|):只问"根目标的词还在不在",子任务多写模板不被长度惩罚;
///   · jaccard 保留给兄弟冗余(对称量,语义正确)。
///
/// 为什么必须降级成 advisory(标定实测,scripts/calibrate_goal_drift.py):
///   真实父子对全集 1222 条 drift_suspect=1.9%,随机错配对照 400 条=81.3%(有分离);
///   但**改写型**父子对(去掉前缀拼接桩与模板桩后剩 39 条,如 T0r61→T0r61.1「依赖图恒空」
///   被写成「增量编译链与依赖图修复:cypyc/incremental/...」)假阳性 56~59%,
///   3 个可测家族上绝对阈值 0.7 命中 15/15——合法拆解与真漂移在词面上不可分。
///   所以:判据照旧发红(离群时最有信息量),但 verdict 一律带 advisory=true,
///   下游按它打回就是把 15/15 的正常子任务反复打回(BUG-25 影响 2 的病灶)。
/// subtask_id 缺省校验根下全部后代。纯计算、只读不写库(决策建议,执行权在 agent/指挥官)。
pub fn FistEngine::goal_drift_check(
  self : FistEngine,
  root_task_id : String,
  subtask_id? : String = "",
  ns? : String = "default",
) -> Json {
  match self.get_task(root_task_id) {
    None =>
      return Json::object({
        "ok": Json::boolean(false),
        "root_task_id": Json::string(root_task_id),
        "note": Json::string("根任务不存在: " + root_task_id),
      })
    Some(rt) => {
      let goal_tokens = @evolve.tokens(rt.get_description())
      let targets : Array[String] = if subtask_id != "" {
        [subtask_id]
      } else {
        self.subtree_ids(root_task_id).filter(fn(id) { id != root_task_id })
      }
      let checks : Array[Json] = []
      let mut aligned = 0
      let mut drift_suspect = 0
      let mut redundant_suspect = 0
      let mut insufficient = 0
      for id in targets {
        match self.get_task(id) {
          None => ()
          Some(t) => {
            let tk = @evolve.tokens(t.get_description())
            let red = self.sibling_max_similarity(t, tk)
            // 信息量不足(空描述/纯模板桩)判 insufficient,不判漂移——
            // 恒阳性正是本条缺陷的形状(BUG-25:6/6 全误报里就混着这一类)
            let low_info = goal_tokens.length() < drift_min_tokens ||
              tk.length() < drift_min_tokens
            let drift = if low_info {
              1.0
            } else {
              1.0 - drift_similarity(goal_tokens, tk)
            }
            let verdict = if low_info {
              "insufficient"
            } else if drift > drift_threshold {
              "drift_suspect"
            } else if red >= redundancy_threshold {
              "redundant_suspect"
            } else {
              "aligned"
            }
            if verdict == "drift_suspect" {
              drift_suspect = drift_suspect + 1
            } else if verdict == "redundant_suspect" {
              redundant_suspect = redundant_suspect + 1
            } else if verdict == "insufficient" {
              insufficient = insufficient + 1
            } else {
              aligned = aligned + 1
            }
            let cm = Map::new()
            cm.set("subtask_id", Json::string(id))
            cm.set("drift_score", Json::number(drift))
            cm.set("redundancy_score", Json::number(red))
            cm.set("verdict", Json::string(verdict))
            // advisory 恒真:本判据在中文改写型语料上假阳性 15/15,不可作打回硬门(BUG-25)
            cm.set("advisory", Json::boolean(true))
            cm.set("re_anchor", Json::boolean(verdict == "drift_suspect"))
            checks.push(Json::object(cm))
          }
        }
      }
      Json::object({
        "ok": Json::boolean(true),
        "root_task_id": Json::string(root_task_id),
        "basis": Json::string(
          "max(jaccard, coverage)@evolve(词法相似度纯计算,零 LLM 自评;coverage 非对称、不罚长度)",
        ),
        "hard_gate": Json::boolean(false),
        "threshold": Json::object({
          "drift": Json::number(drift_threshold),
          "redundancy": Json::number(redundancy_threshold),
          "min_tokens": Json::number(drift_min_tokens.to_double()),
        }),
        "calibration": Json::object({
          "corpus": Json::string(
            "fist-mbt.db 只读实测(2026-09-27),复跑:python scripts/calibrate_goal_drift.py",
          ),
          "real_pairs_suspect": Json::number(0.019),
          "real_pairs_n": Json::number(1222.0),
          "random_control_suspect": Json::number(0.813),
          "random_control_n": Json::number(400.0),
          "reworded_pairs_suspect": Json::number(0.590),
          "reworded_pairs_n": Json::number(39.0),
          "conclusion": Json::string(
            "全量分离成立(1.9% vs 81.3%),但改写型父子对 59% 假阳性 ⇒ 只可 advisory,硬用会把正常拆解反复打回",
          ),
        }),
        "counts": Json::object({
          "aligned": Json::number(aligned.to_double()),
          "drift_suspect": Json::number(drift_suspect.to_double()),
          "redundant_suspect": Json::number(redundant_suspect.to_double()),
          "insufficient": Json::number(insufficient.to_double()),
        }),
        "checks": Json::array(checks),
        "note": Json::string(
          "全局目标校验(advisory,非硬门):drift=1-max(jaccard,coverage)(根目标,子任务) >0.7 记 drift_suspect(附 re_anchor 提示);与兄弟 jaccard ≥0.7 记 redundant_suspect;任一侧词数 <6 记 insufficient(不判漂移)。打回决策需人工/指挥官复核——中文改写型子任务上本判据假阳性 15/15(见 calibration)。",
        ),
      })
    }
  }
}

///|
/// 判据常量(与标定脚本同源,改这里=改判据,必须重跑 scripts/calibrate_goal_drift.py)
let drift_threshold : Double = 0.7

///|
let redundancy_threshold : Double = 0.7

///|
let drift_min_tokens : Int = 6

///|
fn drift_similarity(a : Array[String], b : Array[String]) -> Double {
  max_of(@evolve.jaccard(a, b), @evolve.coverage(a, b))
}

///|
/// 同父兄弟里与本任务的最大词法相似度(对称量)。
/// 取两个口径的最大值:二字组词集抓"用词相同",可见字符集抓"词序换位"
/// (`数据采集模块` ↔ `采集数据模块` 在前者只有 0.556,后者 1.0);
/// 标定实测该分量在真实非模板家族上与单口径同为 34.8%,不额外增误报。
fn FistEngine::sibling_max_similarity(
  self : FistEngine,
  t : @core.Task,
  tk : Array[String],
) -> Double {
  let mut red = 0.0
  match t.get_parent() {
    Some(p) =>
      for sib in self.children_of(p) {
        if sib.get_id() != t.get_id() {
          let sd = sib.get_description()
          let s = max_of(
            @evolve.jaccard(tk, @evolve.tokens(sd)),
            @evolve.jaccard(
              @evolve.unigrams(t.get_description()),
              @evolve.unigrams(sd),
            ),
          )
          if s > red {
            red = s
          }
        }
      }
    None => ()
  }
  red
}

///|
fn max_of(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}