// engine_learn.mbt
// =====================================================================
// P10: 从 engine.mbt 拆出 — 同包(@lib)多文件,调用点零修改。
// 主题: remember/observe/predict/recall + value breakdown + 偏置/对比学习/attention/mark_term/reward (D1–D8 学习+预测核心)

// ---------------------------------------------------------------------------
// D1 突触图谱:写入 + 建边 + 赫布学习
// ---------------------------------------------------------------------------

pub fn ProphecyEngine::remember(
  self : ProphecyEngine,
  text : String,
  mtype : String,
  ctx : Array[String],
) -> String {
  self.clock = self.clock + 1.0
  // P0 性能:分词一次,vec / tm_toks / tm_toks_set 共用(原版 new/merge 分支各自再分词)
  let toks = yimai_tokenize(text)
  let vec = tf_vector(toks)
  let mut best = ""
  let mut best_sim = 0.0
  for mid, m in self.memories.iter2() {
    let s = cosine(vec, m.vec)
    if s > best_sim {
      best = mid
      best_sim = s
    }
  }
  if best != "" && best_sim > DEDUP_SIM {
    let m = self.memories[best]
    m.text = text
    m.vec = vec
    // 若合并目标是 TM 节点,text 已改写,须同步重建特征缓存并刷新 IDF(P2-2)
    if m.mtype == "tm" {
      m.tm_toks = toks
      m.tm_toks_set = to_set(toks)
      m.tm_ngrams = char_ngram_set(m.text, 2)
      self.mark_tm_idf_dirty()
    }
    if m.is_term {
      self.mark_term_index_dirty()   // P4-2:术语 text 被改写,首字符索引可能失配
    }
    m.use_count = m.use_count + 1
    m.last_used = self.clock
    m.last_active = self.clock
    // role_index 增量维护:对 text 全部角色粒度 set 到合并节点(role_index 是序列化字段,
    // 保持与新建分支 L713-717 的一致,非主角色条目同样重指向 best)
    self.role_index.set(role_of(text), best)
    for rk in roles_of(text) {
      self.role_index.set(rk, best)
    }
    self.memories.set(best, m)
    // P4-1:text 已改写,role_members 全量重建(清掉旧角色残留 mid;本索引内部态不进
    // to_json,只做角色限定召回的剪枝,幂等/确定性零影响;与 consolidate/restore 同源)
    self.rebuild_role_members()
    self._wire(best, ctx)
    self.wal_append("remember" + WAL_SEP + best + WAL_SEP + wal_escape_field(text) + WAL_SEP + wal_escape_field(mtype))
    self.invalidate_pred_cache()
    self.fed_upd = self.fed_upd + 1
    return best
  }
  let mid = self._new_id()
  let node : MemoryNode = {
    id: mid,
    text,
    mtype,
    vec,
    created: self.clock,
    last_used: self.clock,
    last_active: self.clock,
    use_count: 1,
    feedback: 0.0,
    edges: Map::from_iter(([] : Array[(String, Double)]).iter()),
    predictive_value: 0.0,
    hit_count: 0,
    predict_count: 0,
    is_term: false,
    translation: "",
    tm_toks: if mtype == "tm" { toks } else { [] },
    tm_toks_set: if mtype == "tm" { to_set(toks) } else { Map([]) },
    tm_ngrams: if mtype == "tm" { char_ngram_set(text, 2) } else { Map([]) },
  }
  if mtype == "tm" {
    self.mark_tm_idf_dirty()
  }
  self.role_index.set(role_of(text), mid)
  for rk in roles_of(text) {
    self.role_index.set(rk, mid)
    self.role_members_add(rk, mid)
  }
  self.wal_append("remember" + WAL_SEP + mid + WAL_SEP + wal_escape_field(text) + WAL_SEP + wal_escape_field(mtype))
  self.invalidate_pred_cache()
  self.memories.set(mid, node)
  self._wire(mid, ctx)
  self.stats_remembers = self.stats_remembers + 1
  self.fed_add = self.fed_add + 1
  mid
}

// 概念抽象:角色抽取(前 4 字)
// role_of 已在 util.mbt 定义

// 共现即建边:一起出现的记忆,其突触加权(fire together, wire together)
fn ProphecyEngine::_wire(self : ProphecyEngine, mid : String, ctx : Array[String]) -> Unit {
  if ctx.length() == 0 {
    return
  }
  for a in ctx {
    if a == mid {
      continue
    }
    match self.memories.get(a) {
      None => ()
      Some(_) => {
        let m = self.memories[mid]
        let w = match m.edges.get(a) { Some(x) => x; None => 0.0 }
        m.edges.set(a, clamp01(w + self.hebb_lr * (1.0 - w)))
        self.memories.set(mid, m)
        let na = self.memories[a]
        let w2 = match na.edges.get(mid) { Some(x) => x; None => 0.0 }
        na.edges.set(mid, clamp01(w2 + self.hebb_lr * (1.0 - w2)))
        self.memories.set(a, na)
      }
    }
  }
}

// ---------------------------------------------------------------------------
// D2 激活扩散召回
// ---------------------------------------------------------------------------

fn pack_recall(
  m : MemoryNode,
  score : Double,
  act : Double,
  via : Array[Json],
) -> Json {
  obj([
    ("id", str_json(m.id)),
    ("text", str_json(m.text)),
    ("type", str_json(m.mtype)),
    ("score", num_json(r4(score))),
    ("activation", num_json(r4(act))),
    ("via_edges", arr_json(via)),
  ])
}

pub fn ProphecyEngine::recall(
  self : ProphecyEngine,
  query : String,
  k : Int,
) -> Array[Json] {
  let qvec = tf_vector(yimai_tokenize(query))
  // 降级:网络过小则退化为 1.0 余弦 Top-K
  if self.memories.length() < MIN_GRAPH {
    let scored : Array[(String, Double)] = []
    for mid, m in self.memories.iter2() {
      scored.push((mid, cosine(qvec, m.vec)))
    }
    sort_desc_by_key(scored, fn(x){ x.1 })
    let out : Array[Json] = []
    let lim = if k < scored.length() { k } else { scored.length() }
    for i = 0; i < lim; i = i + 1 {
      let mid = scored[i].0
      match self.memories.get(mid) {
        Some(m) => out.push(pack_recall(m, 0.0, 0.0, []))
        None => ()
      }
    }
    return out
  }
  // 种子:先按角色索引(role_members)剪枝候选池(S2),空池降级全图
  let role_keys = roles_of(query)
  let pool : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
  for rk in role_keys {
    match self.role_members.get(rk) {
      Some(mids) =>
        for mid in mids {
          pool.set(mid, true)
        }
      None => ()
    }
  }
  let sims : Array[(String, Double)] = []
  if pool.length() > 0 {
    for mid, _ in pool.iter2() {
      match self.memories.get(mid) {
        Some(m) => {
          let s = cosine(qvec, m.vec)
          if s > 0.05 {
            sims.push((mid, s))
          }
        }
        None => ()
      }
    }
  } else {
    // 降级:全图扫描(角色索引为空时保持原行为)
    for mid, m in self.memories.iter2() {
      let s = cosine(qvec, m.vec)
      if s > 0.05 {
        sims.push((mid, s))
      }
    }
  }
  if sims.length() == 0 {
    let all : Array[(String, Int)] = []
    for mid, m in self.memories.iter2() {
      all.push((mid, m.use_count))
    }
    sort_desc_by_key(all, fn(x){ x.1.to_double() })
    for i = 0; i < 3 && i < all.length(); i = i + 1 {
      sims.push((all[i].0, 0.05))
    }
  }
  sort_desc_by_key(sims, fn(x){ x.1 })
  let seeds : Array[String] = []
  for i = 0; i < 3 && i < sims.length(); i = i + 1 {
    seeds.push(sims[i].0)
  }
  // 扩散
  let mut act : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
  for s in seeds {
    // 术语节点高初始激活(+5.0),使其在联想召回中优先浮出(美:术语命中高亮)
    let boost = match self.memories.get(s) {
      Some(m) => if m.is_term { 5.0 } else { 1.0 }
      None => 1.0
    }
    act.set(s, boost)
  }
  for _step = 0; _step < SPREAD_STEPS; _step = _step + 1 {
    let newm : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
    for nid, a in act.iter2() {
      newm.set(nid, a)
    }
    for nid, a in act.iter2() {
      match self.memories.get(nid) {
        Some(m) =>
          for nb, w in m.edges.iter2() {
            // 注意力边权:相似度 + 角色匹配调制扩散(attn_alpha/beta=0 时退化为原行为)
            let attn = if self.attn_alpha == 0.0 && self.attn_beta == 0.0 {
              1.0
            } else {
              match self.memories.get(nb) {
                Some(nbm) => {
                  let simqn = cosine(qvec, nbm.vec)
                  let rmatch = if role_of(m.text) == role_of(nbm.text) { 1.0 } else { 0.0 }
                  @math.exp(self.attn_alpha * simqn + self.attn_beta * rmatch)
                }
                None => 1.0
              }
            }
            let contrib = a * w * SPREAD_DECAY * attn
            if contrib > 0.01 {
              let cur = match newm.get(nb) { Some(x) => x; None => 0.0 }
              newm.set(nb, if contrib > cur { contrib } else { cur })
            }
          }
        None => ()
      }
    }
    act = newm
  }
  // 排序:激活 × (0.3 + 0.7*预测价值)
  let scored : Array[(String, Double, Double, Array[Json])] = []
  for mid, a in act.iter2() {
    match self.memories.get(mid) {
      Some(m) => {
        let score = a * (0.3 + 0.7 * m.predictive_value)
        let via : Array[Json] = []
        for nb, w in m.edges.iter2() {
          if act.contains(nb) {
            via.push(obj([("to", str_json(nb)), ("w", num_json(r4(w)))]))
          }
        }
        scored.push((mid, score, a, via))
      }
      None => ()
    }
  }
  sort_desc_by_key(scored, fn(x){ x.1 })
  let out : Array[Json] = []
  let lim = if k < scored.length() { k } else { scored.length() }
  for i = 0; i < lim; i = i + 1 {
    let (mid, score, a, via) = scored[i]
    match self.memories.get(mid) {
      Some(m) => out.push(pack_recall(m, score, a, via))
      None => ()
    }
  }
  out
}

// ---------------------------------------------------------------------------
// D3 预测前向模型 + D7 不确定性
// ---------------------------------------------------------------------------

// P4 重构:把 predict 269 行拆为 3 段私有 fn
//   - collect_activations: 上下文→激活权重(最近项权重最高)
//   - aggregate_transitions: 一阶转移 + D8 角色回退聚合到 score+path
//   - rank_and_pack: 归一化 + 排名 + 熵 + pack Json(含 predict_count 积累)
//   predict 主体只做缓存、orchestrator 与缓存写;保留 R15 排序依赖(Map 插入序)

// 收集上下文激活:context 中每个 mid 获得 (1+1/n)^exp 形式的近期权重(取最大,重复 mid 不重复加权)
fn ProphecyEngine::predict_collect_activations(self : ProphecyEngine) -> Map[String, Double] {
  let act : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
  let n = self.context.length()
  if n == 0 { return act }
  let exp = 2.0 - self.explore               // 元认知:探索度高时降低幂次->更广利用
  for i = 0; i < n; i = i + 1 {
    let mid = self.context[i]
    match self.memories.get(mid) {
      Some(_) => {
        let base = (i + 1).to_double() / n.to_double()
        let w = @math.pow(base, exp)
        let cur = match act.get(mid) { Some(x) => x; None => 0.0 }
        act.set(mid, if w > cur { w } else { cur })
      }
      None => ()
    }
  }
  act
}

// 一阶转移 + D8 角色回退聚合到 score + path(in-place 修改)
//   转移动作(含 Top-8 剪枝快路径)后,紧接 D8 概念抽象回退:具体转移稀疏时用
//   角色级 (role_trans × role_index) 规律补位,权重 0.5。
fn ProphecyEngine::predict_aggregate_transitions(
  self : ProphecyEngine,
  act : Map[String, Double],
  score : Map[String, Double],
  path : Map[String, Array[Json]],
) -> Unit {
  for a, aw in act.iter2() {
    let tt : Double = match self.transitions.get(a) {
      Some(trans) => {
        let mut tot = 0.0
        for _, c in trans.iter2() {
          tot = tot + c
        }
        let tt0 = if tot == 0.0 { 1.0 } else { tot }
        // 快:每源 Top-K(=8) 剪枝候选,大图下显著降低聚合开销(≤8 时不触发,保证小图行为不变)
        let keep : Array[String] = []
        if trans.length() > 8 {
          let allc : Array[(String, Double)] = []
          for dst, c in trans.iter2() {
            allc.push((dst, c))
          }
          sort_desc_by_key(allc, fn(x){ x.1 })
          let kp = if 8 < allc.length() { 8 } else { allc.length() }
          for i = 0; i < kp; i = i + 1 {
            keep.push(allc[i].0)
          }
        }
        for dst, c in trans.iter2() {
          if keep.length() > 0 && !arr_contains(keep, dst) {
            continue
          }
          let p = c / tt0
          let cur = match score.get(dst) { Some(x) => x; None => 0.0 }
          score.set(dst, cur + aw * p)
          let arr = match path.get(dst) {
            Some(x) => x
            None => {
              let na : Array[Json] = []
              path.set(dst, na)
              na
            }
          }
          arr.push(obj([("from", str_json(a)), ("p", num_json(r4(p)))]))
        }
        tt0
      }
      None => 1.0
    }
    // D8 概念抽象回退:具体转移稀疏(冷启动)时用角色级规律补位
    // 注意:无具体转移时 tt=1.0,同样进入回退(对应 D8 角色级补位的空转移兜底)
    if tt <= 1.0 {
      match self.memories.get(a) {
        Some(na) => {
          for ra in roles_of(na.text) {
            if ra == "" {
              continue
            }
            match self.role_trans.get(ra) {
              Some(rtrans) => {
                let mut rtot = 0.0
                for _, c in rtrans.iter2() {
                  rtot = rtot + c
                }
                let rtt = if rtot == 0.0 { 1.0 } else { rtot }
                for rb, c in rtrans.iter2() {
                  match self.role_index.get(rb) {
                    Some(tgt) =>
                      match self.memories.get(tgt) {
                        Some(_) => {
                          let rp = c / rtt
                          let cur = match score.get(tgt) { Some(x) => x; None => 0.0 }
                          score.set(tgt, cur + aw * rp * 0.5)
                          let arr = match path.get(tgt) {
                            Some(x) => x
                            None => {
                              let na2 : Array[Json] = []
                              path.set(tgt, na2)
                              na2
                            }
                          }
                          arr.push(obj([("from", str_json(a + "(role:" + ra + ")")), ("p", num_json(r4(rp)))]))
                        }
                        None => ()
                      }
                    None => ()
                  }
                }
              }
              None => ()
            }
          }
        }
        None => ()
      }
    }
  }
  // 二阶马尔可夫:以最近一对 (prev2, prev1) 作上下文,叠加 P(w3|w1,w2)
  let nctx = self.context.length()
  if nctx >= 2 {
    let p1 = self.context[nctx - 1]
    let p2 = self.context[nctx - 2]
    let key2 = p2 + "::" + p1
    match self.trans2.get(key2) {
      Some(t2) => {
        let mut tot2 = 0.0
        for _, c in t2.iter2() {
          tot2 = tot2 + c
        }
        let tt2 = if tot2 == 0.0 { 1.0 } else { tot2 }
        let lam = 0.4
        for w3, c in t2.iter2() {
          let p = c / tt2
          let cur = match score.get(w3) { Some(x) => x; None => 0.0 }
          score.set(w3, cur + lam * p)
          let arr = match path.get(w3) {
            Some(x) => x
            None => {
              let na3 : Array[Json] = []
              path.set(w3, na3)
              na3
            }
          }
          arr.push(obj([("from", str_json(p2 + ">" + p1 + "(2nd)")), ("p", num_json(r4(p)))]))
        }
      }
      None => ()
    }
  }
  // 领域偏置 ΔW:按目标节点多粒度角色原型注入先验(LoRA 式;初值为 0 不影响基线)
  let sbias : Array[String] = []
  for d, _ in score.iter2() {
    sbias.push(d)
  }
  for d in sbias {
    match self.memories.get(d) {
      Some(mn) => {
        let mut bias = 0.0
        for rk in roles_of(mn.text) {
          bias = bias + (match self.domain_bias.get(rk) { Some(x) => x; None => 0.0 })
        }
        if bias != 0.0 {
          let cur = match score.get(d) { Some(x) => x; None => 0.0 }
          score.set(d, cur + bias)
        }
      }
      None => ()
    }
  }
  // 归一化(in-place 修改 score)
  let mut z = 0.0
  for _, v in score.iter2() {
    z = z + v
  }
  let znorm = if z == 0.0 { 1.0 } else { z }
  let ks : Array[String] = []
  for d, _ in score.iter2() {
    ks.push(d)
  }
  for d in ks {
    let v = match score.get(d) { Some(x) => x; None => 0.0 }
    score.set(d, v / znorm)
  }
}

// 排名 + 熵 + pack Json;副作用:更新 self.stats_preds/last_pred、topk mid 的 predict_count
fn ProphecyEngine::predict_rank_and_pack(
  self : ProphecyEngine,
  score : Map[String, Double],
  path : Map[String, Array[Json]],
  k : Int,
) -> (Json, Array[String]) {
  // 排名
  let ranked : Array[(String, Double)] = []
  for d, v in score.iter2() {
    ranked.push((d, v))
  }
  sort_desc_by_key(ranked, fn(x){ x.1 })
  let topk : Array[String] = []
  let lim = if k < ranked.length() { k } else { ranked.length() }
  for i = 0; i < lim; i = i + 1 {
    topk.push(ranked[i].0)
  }
  if topk.length() > 0 {
    self.stats_preds = self.stats_preds + 1
    self.last_pred = topk
  } else {
    self.last_pred = []
  }
  // 预测效用积累
  for d in topk {
    match self.memories.get(d) {
      Some(m) => {
        m.predict_count = m.predict_count + 1
        self.memories.set(d, m)
      }
      None => ()
    }
  }
  // D7 不确定性:基于【全量归一化分布】的熵
  //    (F3:原实现仅取 Top-K,极端偏态分布下严重低估不确定性)
  let mut entropy = 0.0
  for i = 0; i < ranked.length(); i = i + 1 {
    let p = ranked[i].1
    if p > 0.0 {
      entropy = entropy - p * @math.ln(p + 0.000000001)
    }
  }
  let preds : Array[Json] = []
  for i = 0; i < lim; i = i + 1 {
    let (mid, p) = ranked[i]
    match self.memories.get(mid) {
      Some(m) =>
        preds.push(obj([
          ("id", str_json(mid)),
          ("text", str_json(m.text)),
          ("prob", num_json(r4(p))),
          ("path", arr_json(match path.get(mid) { Some(x) => x; None => [] })),
        ]))
      None => ()
    }
  }
  let out = obj([
    ("predictions", arr_json(preds)),
    ("confidence", num_json(if preds.length() > 0 { r4(ranked[0].1) } else { 0.0 })),
    ("uncertainty", num_json(r4(entropy))),
  ])
  (out, topk)
}

pub fn ProphecyEngine::predict(self : ProphecyEngine, k : Int) -> Json {
  // 快:预测缓存(相同上下文签名且引擎未变更时直接返回,跳过转移聚合;引擎变更时全量失效,非 LRU)
  let ckeyb = StringBuilder()
  for i = 0; i < self.context.length(); i = i + 1 {
    if i > 0 {
      ckeyb.write_char('|')
    }
    ckeyb.write_string(self.context[i])
  }
  let ckey = ckeyb.to_string() + "|k" + k.to_string()
  match self.pred_cache.get(ckey) {
    Some(cached) => {
      let out = get_obj(cached, "out")
      let topk = get_str_arr(cached, "topk")
      if topk.length() > 0 {
        self.stats_preds = self.stats_preds + 1
        self.last_pred = topk
      } else {
        self.last_pred = []
      }
      for d in topk {
        match self.memories.get(d) {
          Some(m) => {
            m.predict_count = m.predict_count + 1
            self.memories.set(d, m)
          }
          None => ()
        }
      }
      return out
    }
    None => ()
  }
  // 3 段编排(参见对应 fn 注释):collect → aggregate (含 2nd-order + 偏置 + 归一化) → rank+pack
  let act = self.predict_collect_activations()
  let score : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
  let path : Map[String, Array[Json]] =
    Map::from_iter(([] : Array[(String, Array[Json])]).iter())
  self.predict_aggregate_transitions(act, score, path)
  let (out, topk) = self.predict_rank_and_pack(score, path, k)
  // 容量守卫:pred_cache 超上限时清空(内部态,零确定性影响;防长跑无界增长)
  if self.pred_cache.length() >= PRED_CACHE_MAX {
    self.pred_cache = Map::from_iter(([] : Array[(String, Json)]).iter())
  }
  self.pred_cache.set(ckey, obj([("out", out), ("topk", arr_json(topk.map(fn(s) { str_json(s) })))]))
  out
}

// ---------------------------------------------------------------------------
// observe:记住真实下一步 + 更新转移 + 命中核算
// ---------------------------------------------------------------------------

pub fn ProphecyEngine::observe(
  self : ProphecyEngine,
  text : String,
  mtype : String,
) -> String {
  // 时钟语义(P4-1 修正):一次 observe 推进一个逻辑时刻(+1)。
  // remember 内部已自增(L652),故此处不再单独自增,避免每次 observe 竟 +2。
  let mid = self.remember(text, mtype, self.context)
  // D3 转移计数:仅由「紧邻前驱」建立一阶马尔可夫边
  if self.context.length() > 0 {
    let prev = self.context[self.context.length() - 1]
    match self.memories.get(prev) {
      Some(_) => {
        inc_trans(self.transitions, prev, mid)
        // 二阶马尔可夫:最近一对 (prev2, prev) -> mid
        if self.context.length() >= 2 {
          let prev2 = self.context[self.context.length() - 2]
          inc_trans(self.trans2, prev2 + "::" + prev, mid)
        }
        // D8 概念抽象:同时记录多粒度角色级转移
        match self.memories.get(prev) {
          Some(pn) =>
            match self.memories.get(mid) {
              Some(mn) => {
                let ras = roles_of(pn.text)
                let rbs = roles_of(mn.text)
                for ra in ras {
                  if ra == "" {
                    continue
                  }
                  for rb in rbs {
                    if rb != "" {
                      inc_trans(self.role_trans, ra, rb)
                    }
                  }
                }
              }
              None => ()
            }
          None => ()
        }
      }
      None => ()
    }
  }
  // 命中核算(元认知滚动窗口)
  if self.last_pred.length() > 0 {
    let hit = arr_contains(self.last_pred, mid)
    if hit {
      self.stats_hits = self.stats_hits + 1
        match self.memories.get(mid) {
          Some(m) => {
            m.hit_count = m.hit_count + 1
            m.last_active = self.clock
            self.memories.set(mid, m)
          }
          None => ()
        }
    }
    self.meta_hits.push(if hit { 1 } else { 0 })
    if self.meta_hits.length() > 200 {
      let _ = self.meta_hits.remove(0)
    }
    self.last_pred = []
  }
  self.cur_episode.push(mid)
  self.context.push(mid)
  if self.context.length() > CTX_WINDOW {
    let _ = self.context.remove(0)
  }
  self.wal_append("observe" + WAL_SEP + mid + WAL_SEP + wal_escape_field(text) + WAL_SEP + wal_escape_field(mtype))
  self.fed_upd = self.fed_upd + 1
  self.invalidate_pred_cache()
  mid
}

pub fn ProphecyEngine::end_episode(self : ProphecyEngine) -> Unit {
  if self.cur_episode.length() > 0 {
    self.episodes.push(self.cur_episode)
    self.cur_episode = []
  }
}

// ---------------------------------------------------------------------------
// D4 预测价值定价
// ---------------------------------------------------------------------------

// 节点命中率:predict_count>0 时取 hit/predict,否则 0(多处复用的确定性公式)
fn hit_rate_of(m : MemoryNode) -> Double {
  if m.predict_count > 0 {
    m.hit_count.to_double() / m.predict_count.to_double()
  } else {
    0.0
  }
}

// 预测价值分解(D4)。单一事实源,供 _recompute_values(写回 predictive_value)
// 与 explain_card(白盒展示)共用,消除原先两处重复的内联计算。
fn ProphecyEngine::value_breakdown(
  self : ProphecyEngine,
  m : MemoryNode,
) -> ValueBreakdown {
  let now = self.clock
  let u_freq = m.use_count.to_double() / (m.use_count.to_double() + 5.0)
  let dt = now - m.last_used
  let u_rec = if dt < 0.0 { 1.0 } else { clamp01(@math.exp(-dt / REC_TAU)) }
  let u_fb = clamp01((m.feedback + 1.0) / 2.0)
  let u_past = 0.5 * u_freq + 0.3 * u_rec + 0.2 * u_fb
  let hr = hit_rate_of(m)
  let mut tot = 0.0
  match self.transitions.get(m.id) {
    Some(trans) => for _, c in trans.iter2() { tot = tot + c }
    None => ()
  }
  let out_str = min1(tot / (tot + 10.0))
  let u_pred = 0.6 * hr + 0.4 * out_str
  let mut esum = 0.0
  for _, w in m.edges.iter2() {
    esum = esum + w
  }
  let c_graph = min1(esum / 5.0)
  {
    u_freq: u_freq,
    u_rec: u_rec,
    u_fb: u_fb,
    u_past: u_past,
    u_pred: u_pred,
    c_graph: c_graph,
  }
}

fn ProphecyEngine::_recompute_values(self : ProphecyEngine) -> Unit {
  for mid, m in self.memories.iter2() {
    let vb = self.value_breakdown(m)
    let cost = @math.ln(1.0 + self.memories.length().to_double()) / 20.0
    let vval =
      ALPHA * vb.u_past + BETA * vb.u_pred + GAMMA * vb.c_graph + DELTA * vb.u_fb - EPS * cost
    let m2 = m
    m2.predictive_value = clamp01(if vval > 0.0 { vval } else { 0.0 })
    self.memories.set(mid, m2)
  }
}

// ---------------------------------------------------------------------------
// 进化引擎·感知器:成功/失败信号进入预测价值
// ---------------------------------------------------------------------------

pub fn ProphecyEngine::reward(
  self : ProphecyEngine,
  mid : String,
  score_in : Double,
) -> Bool {
  match self.memories.get(mid) {
    Some(m) => {
      let s = if score_in > 1.0 {
        1.0
      } else if score_in < -1.0 {
        -1.0
      } else {
        score_in
      }
      let m2 = m
      m2.feedback = 0.7 * m2.feedback + 0.3 * s
      self.memories.set(mid, m2)
      self._recompute_values()
      self.invalidate_pred_cache()
      self.fed_upd = self.fed_upd + 1
      true
    }
    None => false
  }
}

// ---------------------------------------------------------------------------
// #19 准:领域偏置 ΔW / 在线对比学习 / 注意力边权(对外接口)
// ---------------------------------------------------------------------------

// 领域偏置 ΔW:按角色原型注入先验(外部蒸馏 / 对比学习收敛后写入)
pub fn ProphecyEngine::set_domain_bias(
  self : ProphecyEngine,
  role : String,
  delta : Double,
) -> Unit {
  if role == "" {
    return
  }
  self.domain_bias.set(role, delta)
  self.invalidate_pred_cache()
}

// 在线对比学习单步:增强 (anchor,positive),抑制 (anchor,negative)
// 调用方可据 D7 不确定性动态调节 lr;高不确定时对比信号更强
pub fn ProphecyEngine::cl_step(
  self : ProphecyEngine,
  anchor : String,
  positive : String,
  negative : String,
) -> Unit {
  match self.memories.get(anchor) {
    Some(a) => {
      let w = match a.edges.get(positive) { Some(x) => x; None => 0.0 }
      a.edges.set(positive, clamp01(w + self.hebb_lr))
      self.memories.set(anchor, a)
    }
    None => ()
  }
  match self.memories.get(positive) {
    Some(p) => {
      let w = match p.edges.get(anchor) { Some(x) => x; None => 0.0 }
      p.edges.set(anchor, clamp01(w + self.hebb_lr))
      self.memories.set(positive, p)
    }
    None => ()
  }
  match self.memories.get(anchor) {
    Some(a) => {
      let w = match a.edges.get(negative) { Some(x) => x; None => 0.0 }
      a.edges.set(negative, clamp01(w - self.hebb_lr * 0.5))
      self.memories.set(anchor, a)
    }
    None => ()
  }
  self.cl_buf.push(
    obj([("anchor", str_json(anchor)), ("pos", str_json(positive)), ("neg", str_json(negative))]),
  )
  self.invalidate_pred_cache()
}

// 注意力边权开关(默认关;ardot 工作台按场景开启以强化语义召回)
pub fn ProphecyEngine::set_attention(
  self : ProphecyEngine,
  alpha : Double,
  beta : Double,
) -> Unit {
  self.attn_alpha = alpha
  self.attn_beta = beta
}

// ---- #20 美:TermNode + 可解释推理卡片 ----

// 标记术语节点:高亮 + 高初始激活(召回优先),并刷新时效
pub fn ProphecyEngine::mark_term(self : ProphecyEngine, mid : String) -> Bool {
  match self.memories.get(mid) {
    Some(m) => {
      m.is_term = true
      m.last_active = self.clock
      self.memories.set(mid, m)
      self.invalidate_pred_cache()
      self.mark_term_index_dirty()   // P4-2:新增术语节点,索引需重建
      self.fed_upd = self.fed_upd + 1
      true
    }
    None => false
  }
}

// 可解释推理卡片:对外契约 JSON(激活路径 / 预测路径 / 价值拆解),供 CLI/前端无耦合消费
pub fn ProphecyEngine::explain_card(self : ProphecyEngine, mid : String) -> Json {
  match self.memories.get(mid) {
    Some(m) => {
      self._recompute_values()
      // 激活路径:谁连到它(入边)
      let in_edges : Array[Json] = []
      for omid, om in self.memories.iter2() {
        match om.edges.get(mid) {
          Some(w) => in_edges.push(obj([("from", str_json(omid)), ("w", num_json(r4(w)))]))
          None => ()
        }
      }
      // 预测路径:它预测什么(出边转移)
      let out_preds : Array[Json] = []
      match self.transitions.get(mid) {
        Some(t) =>
          for dst, c in t.iter2() {
            out_preds.push(obj([("to", str_json(dst)), ("count", num_json(c))]))
          }
        None => ()
      }
      // 价值拆解(D4 各项):复用 value_breakdown 单一事实源,与 _recompute_values 完全一致
      let vb = self.value_breakdown(m)
      obj([
        ("id", str_json(mid)),
        ("text", str_json(m.text)),
        ("is_term", m.is_term.to_json()),
        ("activation_path", arr_json(in_edges)),
        ("prediction_path", arr_json(out_preds)),
        (
          "value_breakdown",
          obj([
            ("u_freq", num_json(r4(vb.u_freq))),
            ("u_recency", num_json(r4(vb.u_rec))),
            ("u_feedback", num_json(r4(vb.u_fb))),
            ("u_past", num_json(r4(vb.u_past))),
            ("u_pred", num_json(r4(vb.u_pred))),
            ("c_graph", num_json(r4(vb.c_graph))),
            ("predictive_value", num_json(r4(m.predictive_value))),
          ]),
        ),
      ])
    }
    None => obj([("error", str_json("unknown id"))])
  }
}