/**
 * 键共振 Destiny · 键/关系引擎（合冲刑害破生克 + 同字共振 + 通根 + 烈度）
 * 依据 AI_FEED_CANON.md（唯一事实源）· 参数冻结 2026-08-11。
 *
 * 口径：
 * · 键距 E = (1/2)^a。原局：同柱／同层邻柱 a=0；同层隔柱 −1；跨层用柱距。
 *   大运对年日月 a=0、对时 a=1。小运对时日 a=0、对月 a=1、对年 a=2。
 *   流年 a=0 → 流时 a=3。
 * · a≥1 只特关合冲，无生克（戌午 a=1、丑午 a=2）。
 * · 流层对原局分隔：不生克。入局只经纠缠（特关）与共振（单字／纠缠／生克）。
 *   流年对大运／小运 a=0，可生克。
 *   单字共振：局中该字能量＋¼×E，不流动。纠缠／生克共振：能量流动，能与烈度都增。
 * · 天干特殊关系仅四冲／五合：可隔位遥感，受 E=(1/2)^a 限制。普通生克仍隔位归零。
 * · 五合化：A−¼B−¼C＋½（C 当令旺／相且不受冲）。五合绊：不聚 C，能量按普通克。
 * · 四冲：A−½B−⅝，与五合同级；共干则五合解冲。
 * · 地支特关分两类（三字亦拆两字）。会合：会／三合／半合（须中神）／六合。
 *   会：只算会成五行的印比总量传递相加，三字藏干含量不变。
 *   三合／半合：库余气×5，只算印比传递，其他藏干不变，库中气闭。
 *   六合：只算合成五行印比传递；合绊按生克；库中另一气锁；无关藏干不变。
 *   刑／冲：开库。害：不开库。半合：已开库则合成行比量相加×E。
 * · 两字间：字内藏干不作用；两字间同行不作用。a=0 先合冲再生克。
 *   会合：合→冲→生→克。冲刑害破：冲→合→克→生。
 *   普通生克须紧贴 a=0：先算藏干特关；若无特关，相生则生→克，相克则克→生。
 * · 凡合冲生克以小量为基数。
 * · 时序：干∥支作用后含量固定 → 通根（同天干藏干字共振＋同柱同行传递）
 *   → 八字五行固定 → 目字十神生链／截断（不改能量，只判流通与截断）。
 * · 烈度分层（canon 5.2）：三会3.0；三合＝三刑2.5；两合＝冲2.0；半合＝两刑1.5；
 *   害1.25；破1.0；生克/同字1.0。最终 ×同键数量×距离E；事显阈值 ≥2.0。
 * · 能量与烈度分账：烈度只定强弱/优选，不改写五行 totals。
 *
 * 注：三刑分组、六害、六破、半合等采用通用命理表；破以「A生B」判向（同五行破按对损处理）。
 *     作者可按内核校订本模块的关系表与判向。
 * © 2026 Jason Li（泽森）
 */
(function (global) {
  "use strict";

  const TG = ["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"];
  const DZ = ["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"];
  const WX = ["木","火","土","金","水"];
  const STEM_WX = [0,0,1,1,2,2,3,3,4,4];
  const BRANCH_WX = [4,2,0,0,2,1,1,2,3,3,2,4];
  const HIDE = {
    子:[["癸",1]], 丑:[["己",0.6],["癸",0.3],["辛",0.1]],
    寅:[["甲",0.6],["丙",0.3],["戊",0.1]], 卯:[["乙",1]],
    辰:[["戊",0.6],["乙",0.3],["癸",0.1]], 巳:[["丙",0.6],["戊",0.3],["庚",0.1]],
    午:[["丁",0.7],["己",0.3]], 未:[["己",0.6],["丁",0.3],["乙",0.1]],
    申:[["庚",0.6],["壬",0.3],["戊",0.1]], 酉:[["辛",1]],
    戌:[["戊",0.6],["辛",0.3],["丁",0.1]], 亥:[["壬",0.7],["甲",0.3]],
  };

  const PKEY = ["year","month","day","time"];
  const PZH = { year:"年", month:"月", day:"日", time:"时" };
  const VAULT = { 辰:1, 戌:1, 丑:1, 未:1 };

  // —— 地支关系表 ——
  const LIUHE = { 子:"丑",丑:"子",寅:"亥",亥:"寅",卯:"戌",戌:"卯",辰:"酉",酉:"辰",巳:"申",申:"巳",午:"未",未:"午" };
  const LIUHE_HUA = { "子丑":"土","寅亥":"木","卯戌":"火","辰酉":"金","巳申":"水","午未":"土" };
  const LIUCHONG = { 子:"午",午:"子",丑:"未",未:"丑",寅:"申",申:"寅",卯:"酉",酉:"卯",辰:"戌",戌:"辰",巳:"亥",亥:"巳" };
  const LIUHAI = { 子:"未",未:"子",丑:"午",午:"丑",寅:"巳",巳:"寅",卯:"辰",辰:"卯",申:"亥",亥:"申",酉:"戌",戌:"酉" };
  const LIUPO = { 子:"酉",酉:"子",卯:"午",午:"卯",申:"巳",巳:"申",寅:"亥",亥:"寅",辰:"丑",丑:"辰",戌:"未",未:"戌" };
  const XING_GROUPS = [ ["寅","巳","申"], ["丑","戌","未"] ]; // 三刑
  const XING_PAIR_ZIMAO = ["子","卯"]; // 子卯相刑
  const ZIXING = ["辰","午","酉","亥"]; // 自刑（同字重见）
  const SANHE = [ ["申","子","辰","水"], ["亥","卯","未","木"], ["寅","午","戌","火"], ["巳","酉","丑","金"] ];
  const SANHUI = [ ["寅","卯","辰","木"], ["巳","午","未","火"], ["申","酉","戌","金"], ["亥","子","丑","水"] ];
  // 半合（生+旺 或 旺+墓），带旺支
  const BANHE = { "申子":"水","子辰":"水","亥卯":"木","卯未":"木","寅午":"火","午戌":"火","巳酉":"金","酉丑":"金" };
  // 天干
  const TIANGAN_HE = { 甲:"己",己:"甲",乙:"庚",庚:"乙",丙:"辛",辛:"丙",丁:"壬",壬:"丁",戊:"癸",癸:"戊" };
  const TIANGAN_HE_HUA = { "甲己":"土","乙庚":"金","丙辛":"水","丁壬":"木","戊癸":"火" };
  const TIANGAN_CHONG = { 甲:"庚",庚:"甲",乙:"辛",辛:"乙",丙:"壬",壬:"丙",丁:"癸",癸:"丁" };

  function pairKey(a, b) { return [a, b].sort().join(""); }
  function lookupHua(table, a, b) {
    return (table && (table[a + b] || table[b + a] || table[pairKey(a, b)])) || "";
  }
  function stemWX(ch) { const i = TG.indexOf(ch); return i < 0 ? -1 : STEM_WX[i]; }
  function branchWX(ch) { const i = DZ.indexOf(ch); return i < 0 ? -1 : BRANCH_WX[i]; }
  function E(a) { return Math.pow(0.5, a); } // 键距

  const LAY_KEY = { 年: "year", 月: "month", 日: "day", 时: "time" };

  /**
   * 合化临界（天干五合）：合得住 ≠ 化得出。可隔位遥感，×E。
   * 月令对化神旺／相且 C 不受冲 → 合而化；休／囚／死或坐支邻冲 → 合绊。
   * 化：A−¼ B−¼ C＋½×E。绊：不聚 C，能量按普通克（攻⅛受⅜，弱方为基数）。
   */
  function hehuaLingState(hua, monthBr) {
    const huaWx = WX.indexOf(hua);
    const monthWx = branchWX(monthBr);
    if (huaWx < 0 || monthWx < 0) return { state: "不明", canHua: false };
    if (monthWx === huaWx) return { state: "旺", canHua: true };
    if ((monthWx + 1) % 5 === huaWx) return { state: "相", canHua: true };
    if ((huaWx + 1) % 5 === monthWx) return { state: "休", canHua: false };
    if ((monthWx + 2) % 5 === huaWx) return { state: "死", canHua: false };
    return { state: "囚", canHua: false };
  }

  function pillarKeyOf(x) {
    if (!x) return "";
    if (typeof x === "string") {
      if (LAY_KEY[x]) return LAY_KEY[x];
      if (PKEY.indexOf(x) >= 0) return x;
      return "";
    }
    if (x.lay && LAY_KEY[x.lay]) return LAY_KEY[x.lay];
    if (x.pillarKey && PKEY.indexOf(x.pillarKey) >= 0) return x.pillarKey;
    return "";
  }

  function hehuaChongPo(pillars, keyA, keyB) {
    if (!pillars) return { po: false, chars: [] };
    const keys = PKEY;
    const idx = { year: 0, month: 1, day: 2, time: 3 };
    const seats = [keyA, keyB].filter(function (k) { return k && pillars[k] && pillars[k][1]; });
    for (let s = 0; s < seats.length; s++) {
      const k = seats[s];
      const br = pillars[k][1];
      const i = idx[k];
      if (i == null || !br) continue;
      for (let d = -1; d <= 1; d += 2) {
        const k2 = keys[i + d];
        if (!k2 || !pillars[k2]) continue;
        const br2 = pillars[k2][1];
        if (br2 && LIUCHONG[br] === br2) return { po: true, chars: [br, br2] };
      }
    }
    return { po: false, chars: [] };
  }

  function hehuaThreshold(hua, monthBr, pillars, charA, charB) {
    const ling = hehuaLingState(hua, monthBr);
    const po = hehuaChongPo(pillars, pillarKeyOf(charA), pillarKeyOf(charB));
    if (po.po) {
      return {
        canHua: false, mode: "ban", ling: ling.state, chongPo: true,
        reason: "合神坐支被邻柱冲破（" + po.chars.join("") + "）",
      };
    }
    if (!ling.canHua) {
      return {
        canHua: false, mode: "ban", ling: ling.state, chongPo: false,
        reason: "月令对化神" + ling.state + "，合而不化",
      };
    }
    return {
      canHua: true, mode: "hua", ling: ling.state, chongPo: false,
      reason: "月令对化神" + ling.state + "，合而化",
    };
  }

  /** 凡合冲生克以小量为基数。 */
  function minOf(out, wxA, wxB) {
    return Math.min(Number(out && out[wxA]) || 0, Number(out && out[wxB]) || 0);
  }

  function applyHeHuaMin(out, wxA, wxB, hua, e, sk) {
    if (!out) return 0;
    const m = minOf(out, wxA, wxB);
    const ee = e == null ? 1 : e;
    const ha = Math.abs(sk && sk.he && sk.he.a != null ? sk.he.a : 0.25);
    const hb = Math.abs(sk && sk.he && sk.he.b != null ? sk.he.b : 0.25);
    const hc = (sk && sk.he && sk.he.c != null) ? sk.he.c : 0.5;
    out[wxA] = Math.max(0, (out[wxA] || 0) - ha * m * ee);
    out[wxB] = Math.max(0, (out[wxB] || 0) - hb * m * ee);
    out[hua] = (out[hua] || 0) + hc * m * ee;
    return m;
  }

  function applyChongMin(out, wxA, wxB, e, sk) {
    if (!out) return 0;
    const m = minOf(out, wxA, wxB);
    const ee = e == null ? 1 : e;
    const atkF = Math.abs(sk && sk.chong && sk.chong.a != null ? sk.chong.a : 0.5);
    const vicF = Math.abs(sk && sk.chong && sk.chong.b != null ? sk.chong.b : 0.625);
    const aWeak = ((out[wxA] || 0) <= (out[wxB] || 0));
    const atk = aWeak ? wxB : wxA;
    const vic = aWeak ? wxA : wxB;
    out[atk] = Math.max(0, (out[atk] || 0) - atkF * m * ee);
    out[vic] = Math.max(0, (out[vic] || 0) - vicF * m * ee);
    return m;
  }

  function applyShengMin(out, genWx, recvWx, e, sk) {
    if (!out) return 0;
    const m = minOf(out, genWx, recvWx);
    const ee = e == null ? 1 : e;
    const loss = Math.abs(sk && sk.sheng && sk.sheng.a != null ? sk.sheng.a : 0.25);
    const gain = (sk && sk.sheng && sk.sheng.b != null) ? sk.sheng.b : 0.25;
    out[genWx] = Math.max(0, (out[genWx] || 0) - loss * m * ee);
    out[recvWx] = (out[recvWx] || 0) + gain * m * ee;
    return m;
  }

  /** 天干五合：化才改道（小量基数）；绊按普通克。 */
  function applyWuHeEnergy(lossFn, gainFn, A, B, e, sk, crit, out) {
    sk = sk || {};
    const hua = lookupHua(TIANGAN_HE_HUA, A.ch, B.ch);
    if (!hua || !crit) return { moved: false, hua: "", mode: "ban" };
    if (crit.canHua) {
      if (out) applyHeHuaMin(out, WX[A.wx], WX[B.wx], hua, e, sk);
      else {
        const ha = (sk.he ? sk.he.a : -0.25);
        const hb = (sk.he ? sk.he.b : -0.25);
        const hc = (sk.he ? sk.he.c : 0.5);
        if (lossFn) lossFn(WX[A.wx], ha, e);
        if (lossFn) lossFn(WX[B.wx], hb, e);
        if (gainFn) gainFn(hua, hc, e);
      }
      return { moved: true, hua: hua, mode: "hua" };
    }
    if (!out) return { moved: false, hua: hua, mode: "ban" };
    let atk = A, vic = B;
    if (keDir(B.wx, A.wx)) { atk = B; vic = A; }
    else if (!keDir(A.wx, B.wx)) return { moved: false, hua: hua, mode: "ban" };
    applyKe(out, WX[atk.wx], WX[vic.wx], e, sk);
    return { moved: true, hua: hua, mode: "ban", via: "ke" };
  }

  function collectGanHeStems(ganList) {
    const he = {};
    for (let i = 0; i < ganList.length; i++) {
      for (let j = i + 1; j < ganList.length; j++) {
        if (TIANGAN_HE[ganList[i].ch] === ganList[j].ch) {
          he[ganList[i].ch] = true;
          he[ganList[j].ch] = true;
        }
      }
    }
    return he;
  }

  function applySiChongEnergy(lossFn, A, B, e, sk, out) {
    if (out) {
      applyChongMin(out, WX[A.wx], WX[B.wx], e, sk);
      return;
    }
    const aWeak = ((out && out[WX[A.wx]]) || 0) <= ((out && out[WX[B.wx]]) || 0);
    const victim = aWeak ? A : B, attacker = aWeak ? B : A;
    if (lossFn) {
      lossFn(WX[attacker.wx], (sk && sk.chong ? sk.chong.a : -0.5), e);
      lossFn(WX[victim.wx], (sk && sk.chong ? sk.chong.b : -0.625), e);
    }
  }

  function hidesOf(br, ctx) {
    const opened = !!(ctx && ctx.openedVaults && ctx.openedVaults[br]);
    return (HIDE[br] || []).map(function (pair) {
      let w = pair[1];
      if (opened && VAULT[br] && w <= 0.1001) w = +(w * 5).toFixed(4);
      return { ch: pair[0], wx: stemWX(pair[0]), w: w };
    });
  }

  function openVault(ctx, br) {
    if (!ctx || !VAULT[br]) return false;
    ctx.openedVaults = ctx.openedVaults || {};
    ctx.openedVaults[br] = true;
    return true;
  }

  /**
   * 原局八字距。同柱／同层邻柱 a=0（丑戌、庚午）。
   * 同层：a=|柱距|−1（癸↔庚=2）。跨层：a=|柱距|（戌↔甲=1，丑↔庚=3）。
   */
  function natalDist(A, B) {
    if (!A || !B) return 99;
    if (A.global || B.global) return 0;
    const pa = A.pillar, pb = B.pillar;
    if (pa == null || pb == null) return 99;
    if (pa === pb) return 0;
    const sameLayer = !A.layer || !B.layer || A.layer === B.layer;
    if (sameLayer) return Math.max(0, Math.abs(pa - pb) - 1);
    return Math.abs(pa - pb);
  }

  function isXiaoYun(c) {
    return !!(c && c.kind === "yun" && /小运/.test(c.lay || c.label || ""));
  }

  function formedTriplePair(a, b, zhiSet) {
    if (!a || !b || a === b || !zhiSet) return false;
    const triples = [];
    SANHE.forEach(function (g) { triples.push(g.slice(0, 3)); });
    SANHUI.forEach(function (g) { triples.push(g.slice(0, 3)); });
    XING_GROUPS.forEach(function (g) { triples.push(g.slice()); });
    return triples.some(function (g) {
      return g.indexOf(a) >= 0 && g.indexOf(b) >= 0 && g.every(function (ch) { return zhiSet.indexOf(ch) >= 0; });
    });
  }

  function zhiPairSpecialTypes(a, b) {
    const types = [];
    const pk = pairKey(a, b);
    if (LIUHE[a] === b) types.push("liuhe");
    else if (BANHE[pk] || BANHE[a + b] || BANHE[b + a]) types.push("banhe");
    if (LIUCHONG[a] === b) types.push("chong");
    if (LIUHAI[a] === b) types.push("hai");
    if (LIUPO[a] === b) types.push("po");
    const inXing = XING_GROUPS.some(function (g) { return g.indexOf(a) >= 0 && g.indexOf(b) >= 0 && a !== b; });
    const ziMao = XING_PAIR_ZIMAO.indexOf(a) >= 0 && XING_PAIR_ZIMAO.indexOf(b) >= 0 && a !== b;
    const ziXing = a === b && ZIXING.indexOf(a) >= 0;
    if (inXing || ziMao || ziXing) types.push("xing");
    return types;
  }

  function isZhiSpecialPair(a, b, zhiSet) {
    return zhiPairSpecialTypes(a, b).length > 0 || formedTriplePair(a, b, zhiSet);
  }

  const ZHONG_SHEN = { 水: "子", 木: "卯", 火: "午", 金: "酉" };

  function inCompletedGroup(a, b, zhiSet, groups) {
    if (!a || !b || !zhiSet) return null;
    for (let i = 0; i < groups.length; i++) {
      const g = groups[i];
      const brs = g.slice(0, 3);
      if (brs.indexOf(a) >= 0 && brs.indexOf(b) >= 0 && brs.every(function (ch) { return zhiSet.indexOf(ch) >= 0; })) {
        return g;
      }
    }
    return null;
  }

  function isHeHuiPair(a, b, zhiSet) {
    if (inCompletedGroup(a, b, zhiSet, SANHUI) || inCompletedGroup(a, b, zhiSet, SANHE)) return true;
    if (LIUHE[a] === b) return true;
    const hua = BANHE[pairKey(a, b)] || BANHE[a + b] || BANHE[b + a];
    if (!hua) return false;
    const zhong = ZHONG_SHEN[hua];
    return !!(zhong && (a === zhong || b === zhong || (zhiSet && zhiSet.indexOf(zhong) >= 0)));
  }

  function isXingChongPair(a, b, zhiSet) {
    if (isHeHuiPair(a, b, zhiSet)) return false;
    const types = zhiPairSpecialTypes(a, b);
    if (types.some(function (t) { return t === "chong" || t === "xing" || t === "hai" || t === "po"; })) return true;
    return XING_GROUPS.some(function (g) {
      return g.indexOf(a) >= 0 && g.indexOf(b) >= 0 && a !== b && g.every(function (ch) { return zhiSet && zhiSet.indexOf(ch) >= 0; });
    });
  }

  function zhiSpecialClass(a, b, zhiSet) {
    if (isHeHuiPair(a, b, zhiSet)) return "hehui";
    if (isXingChongPair(a, b, zhiSet)) return "xingchong";
    return "";
  }

  /** 会成五行的印比：比＝该行，印＝生该行。 */
  function yinBiOfBranch(br, hua, opts) {
    opts = opts || {};
    const huaWx = WX.indexOf(hua);
    const yinWx = huaWx < 0 ? -1 : (huaWx + 4) % 5;
    let hides = hidesOf(br);
    if (VAULT[br] && opts.openYu) {
      hides = hides.map(function (h) {
        if (h.w <= 0.1001 && (h.wx === huaWx || h.wx === yinWx)) return { ch: h.ch, wx: h.wx, w: +(h.w * 5).toFixed(4) };
        return h;
      });
    }
    if (VAULT[br] && opts.lockOther) {
      hides = hides.filter(function (h) { return h.wx === huaWx || h.wx === yinWx; });
    }
    let bi = 0, yin = 0;
    hides.forEach(function (h) {
      if (h.wx === huaWx) bi += h.w;
      else if (h.wx === yinWx) yin += h.w;
    });
    return { bi: +bi.toFixed(4), yin: +yin.toFixed(4), total: +(bi + yin).toFixed(4), hides: hides, hua: hua };
  }

  function isYinBiWx(wx, hua) {
    const huaWx = WX.indexOf(hua);
    if (huaWx < 0) return false;
    return wx === huaWx || wx === (huaWx + 4) % 5;
  }

  /** 印比在字与字之间传递相加。藏干含量不变，五行总量不变。以小量为基数。 */
  function applyYinBiTransfer(brA, brB, hua, e, ctx, opts) {
    const A = yinBiOfBranch(brA, hua, opts);
    const B = yinBiOfBranch(brB, hua, opts);
    const m = Math.min(A.total, B.total);
    const xfer = 0.25 * (e == null ? 1 : e) * m;
    if (ctx && ctx.transfers) {
      ctx.transfers.push({
        kind: "yinbi", hua: hua, from: brA, to: brB,
        aTotal: A.total, bTotal: B.total, min: +m.toFixed(4), xfer: +xfer.toFixed(4),
        formula: "印比传递·小量×¼×e", note: (brA + brB + "会" + hua + "印比"),
      });
    }
    return { a: A, b: B, min: m, xfer: xfer };
  }

  function applyYinBiGroup(brs, hua, e, ctx, opts) {
    const kinds = [];
    for (let i = 0; i < brs.length; i++) {
      for (let j = i + 1; j < brs.length; j++) {
        applyYinBiTransfer(brs[i], brs[j], hua, e, ctx, opts);
        if (kinds.indexOf("yinbi") < 0) kinds.push("yinbi");
      }
    }
    return kinds;
  }

  /**
   * 藏干权重按比例叠进 E（e × wA × wB）。
   */
  function stemAsHide(ch) {
    return { ch: ch, wx: stemWX(ch), w: 1 };
  }

  /** 紧贴：仅 a=0 可生克。a≥1 只特关合冲。流对原局永不紧贴。 */
  function isTightShengKe(A, B, a) {
    if (!A || !B) return false;
    if (flowNatalSeparated(A, B)) return false;
    return (a == null ? 99 : a) <= 0;
  }

  /** 单字共振：局中该字能量增加，不形成流动。 */
  function applyTongziBoost(A, B, e, out, transfers) {
    if (!A || !B || A.ch !== B.ch) return;
    const wx = WX[A.wx];
    if (!wx) return;
    const ee = e == null ? 1 : e;
    const add = 0.25 * ee;
    if (out && out[wx] != null) out[wx] = +(Number(out[wx]) + add).toFixed(4);
    if (transfers) {
      transfers.push({
        kind: "tongzi", from: A.ch, to: B.ch, wx: wx,
        e: +ee.toFixed(4), add: +add.toFixed(4), flow: false,
        formula: "单字共振·局中＋¼×E", note: A.ch + A.ch + "同字不流动",
      });
    }
  }

  function resonateKind(t) {
    if (t === "tongzi") return "tongzi";
    if (t === "sheng" || t === "ke") return "shengke";
    if (ENTANGLE[t]) return "entangle";
    return "";
  }

  /** 同行不生克：先字 −¼×e、后字 ＋¼×e。同五行，总量不变，只记账。 */
  function recordTonghang(ctx, A, B, eEff, note) {
    if (!ctx || !ctx.transfers) return;
    ctx.transfers.push({
      from: A.ch, to: B.ch, wx: WX[A.wx],
      e: +(eEff || 0).toFixed(4),
      frac: +(0.25 * (eEff || 0)).toFixed(4),
      formula: "先−¼×e、后＋¼×e",
      note: note || "同行传递",
    });
  }

  function collectHidePairs(HA, HB, e) {
    const pairs = [];
    (HA || []).forEach(function (ha) {
      (HB || []).forEach(function (hb) {
        const eEff = e * (ha.w || 1) * (hb.w || 1);
        if (eEff <= 0) return;
        if (ha.wx === hb.wx) pairs.push({ kind: "tonghang", ha: ha, hb: hb, eEff: eEff });
        else if (TIANGAN_HE[ha.ch] === hb.ch) pairs.push({ kind: "he", ha: ha, hb: hb, eEff: eEff });
        else if (TIANGAN_CHONG[ha.ch] === hb.ch) pairs.push({ kind: "chong", ha: ha, hb: hb, eEff: eEff });
        else if (shengDir(ha.wx, hb.wx) || shengDir(hb.wx, ha.wx)) pairs.push({ kind: "sheng", ha: ha, hb: hb, eEff: eEff });
        else if (keDir(ha.wx, hb.wx) || keDir(hb.wx, ha.wx)) pairs.push({ kind: "ke", ha: ha, hb: hb, eEff: eEff });
      });
    });
    return pairs;
  }

  function runHidePair(p, sk, critCtx, lossFn, gainFn, out, kinds) {
    if (p.kind === "he") {
      const hua = lookupHua(TIANGAN_HE_HUA, p.ha.ch, p.hb.ch);
      const crit = hua ? hehuaThreshold(hua, critCtx && critCtx.monthBr, critCtx && critCtx.pillars, critCtx && critCtx.keyA, critCtx && critCtx.keyB) : { canHua: false, mode: "ban" };
      applyWuHeEnergy(lossFn, gainFn, p.ha, p.hb, p.eEff, sk, crit, out);
      if (kinds.indexOf("wuhe") < 0) kinds.push("wuhe");
    } else if (p.kind === "chong") {
      applySiChongEnergy(lossFn, p.ha, p.hb, p.eEff, sk, out);
      if (kinds.indexOf("sichong") < 0) kinds.push("sichong");
    } else if (p.kind === "tonghang") {
      recordTonghang(critCtx, p.ha, p.hb, p.eEff, "藏干同行传递");
      if (kinds.indexOf("tonghang") < 0) kinds.push("tonghang");
    } else if (p.kind === "sheng") {
      const gen = shengDir(p.ha.wx, p.hb.wx) ? p.ha : p.hb;
      const recv = gen === p.ha ? p.hb : p.ha;
      if (out) applyShengMin(out, WX[gen.wx], WX[recv.wx], p.eEff, sk);
      else {
        if (lossFn) lossFn(WX[gen.wx], (sk && sk.sheng ? sk.sheng.a : -0.25), p.eEff);
        if (gainFn) gainFn(WX[recv.wx], (sk && sk.sheng ? sk.sheng.b : 0.25), p.eEff);
      }
      if (kinds.indexOf("sheng") < 0) kinds.push("sheng");
    } else if (p.kind === "ke") {
      const atk = keDir(p.ha.wx, p.hb.wx) ? p.ha : p.hb;
      const vic = atk === p.ha ? p.hb : p.ha;
      applyKe(out, WX[atk.wx], WX[vic.wx], p.eEff, sk);
      if (kinds.indexOf("ke") < 0) kinds.push("ke");
    }
  }

  /**
   * 两字间藏干时序。字内不配对（HA×HB）。同行默认不作用。
   * 会合：合→冲→生→克。冲刑害破：冲→合→克→生。
   * 普通：先藏干特关（合冲），若无特关则相生生→克、相克克→生。
   * a>0 只做合冲，不做生克。
   */
  function twoCharHideOrder(cls, pairWxA, pairWxB, includeShengKe) {
    let hechong;
    let shengke;
    if (cls === "hehui") {
      hechong = ["he", "chong"];
      shengke = ["sheng", "ke"];
    } else if (cls === "xingchong") {
      hechong = ["chong", "he"];
      shengke = ["ke", "sheng"];
    } else if (pairWxA != null && pairWxB != null && (keDir(pairWxA, pairWxB) || keDir(pairWxB, pairWxA))) {
      hechong = ["chong", "he"];
      shengke = ["ke", "sheng"];
    } else {
      hechong = ["he", "chong"];
      shengke = ["sheng", "ke"];
    }
    return includeShengKe ? hechong.concat(shengke) : hechong.slice();
  }

  function applyHideHeChongShengKe(HA, HB, e, sk, critCtx, lossFn, gainFn, out, pairWxA, pairWxB, flags) {
    flags = flags || {};
    const skipTonghang = !!flags.skipTonghang;
    const cls = flags.cls || "ordinary";
    const includeShengKe = flags.includeShengKe != null ? !!flags.includeShengKe : (flags.a == null || flags.a <= 0);
    const pairs = collectHidePairs(HA, HB, e);
    const kinds = [];
    let order;
    if (pairWxA != null && pairWxB != null && pairWxA === pairWxB && !skipTonghang) {
      order = ["tonghang"];
    } else {
      order = twoCharHideOrder(cls, pairWxA, pairWxB, includeShengKe);
    }
    if (!skipTonghang && order.indexOf("tonghang") < 0 && pairs.some(function (p) { return p.kind === "tonghang"; })) {
      order.push("tonghang");
    }
    order.forEach(function (kind) {
      pairs.forEach(function (p) {
        if (p.kind !== kind) return;
        if (kind === "tonghang" && skipTonghang) return;
        runHidePair(p, sk, critCtx, lossFn, gainFn, out, kinds);
      });
    });
    return kinds;
  }

  function applyHuaLiangAdd(brA, brB, hua, e, ctx) {
    const huaWx = WX.indexOf(hua);
    function biOf(br) {
      return hidesOf(br, ctx).reduce(function (s, h) { return s + (h.wx === huaWx ? h.w : 0); }, 0);
    }
    const aBi = +biOf(brA).toFixed(4);
    const bBi = +biOf(brB).toFixed(4);
    const sum = +(aBi + bBi).toFixed(4);
    const ee = e == null ? 1 : e;
    if (ctx && ctx.transfers) {
      ctx.transfers.push({
        kind: "huajia", hua: hua, from: brA, to: brB,
        aBi: aBi, bBi: bBi, sum: sum, e: +ee.toFixed(4),
        weighted: +(sum * ee).toFixed(4),
        formula: "合成行比量相加×E", note: (brA + brB + "半合" + hua + "量相加"),
      });
    }
    return { aBi: aBi, bBi: bBi, sum: sum, e: ee };
  }

  function relatedHidePairs(brA, brB, e, hua, ctx) {
    const HA = hidesOf(brA, ctx);
    const HB = hidesOf(brB, ctx);
    return collectHidePairs(HA, HB, e).filter(function (p) {
      if (p.kind === "tonghang" || p.kind === "sheng" || p.kind === "ke") return false;
      if (!hua) return p.kind === "he" || p.kind === "chong";
      return isYinBiWx(p.ha.wx, hua) || isYinBiWx(p.hb.wx, hua);
    });
  }

  function mergeHideKinds(kinds, extra) {
    (extra || []).forEach(function (t) {
      if (kinds.indexOf(t) < 0) kinds.push(t);
    });
    return kinds;
  }

  function applyHeHuiPair(brA, brB, e, sk, critCtx, lossFn, gainFn, out, flags) {
    flags = flags || {};
    critCtx = critCtx || {};
    critCtx.openedVaults = critCtx.openedVaults || {};
    const zhiSet = critCtx.zhiSet || [brA, brB];
    const kinds = [];
    const sanhui = inCompletedGroup(brA, brB, zhiSet, SANHUI);
    const sanhe = inCompletedGroup(brA, brB, zhiSet, SANHE);
    let hua = "";
    let opts = { openYu: false, lockOther: false };
    if (sanhui) {
      hua = sanhui[3];
      applyYinBiTransfer(brA, brB, hua, e, critCtx, opts);
      kinds.push("yinbi");
    } else if (sanhe) {
      hua = sanhe[3];
      opts = { openYu: true, lockOther: true };
      applyYinBiTransfer(brA, brB, hua, e, critCtx, opts);
      kinds.push("yinbi");
    } else if (LIUHE[brA] === brB) {
      hua = lookupHua(LIUHE_HUA, brA, brB);
      opts = { openYu: false, lockOther: true };
      const crit = hua ? hehuaThreshold(hua, critCtx && critCtx.monthBr, critCtx && critCtx.pillars, critCtx && critCtx.keyA, critCtx && critCtx.keyB) : { canHua: false, mode: "ban" };
      if (crit.canHua) {
        applyYinBiTransfer(brA, brB, hua, e, critCtx, opts);
        kinds.push("yinbi");
      } else {
        // 合绊按生克：仅 a=0。隔位 a≥1 只合冲。
        if (flags.a == null || flags.a <= 0) {
          const wa = branchWX(brA), wb = branchWX(brB);
          if (keDir(wa, wb) || keDir(wb, wa)) {
            const atk = keDir(wa, wb) ? WX[wa] : WX[wb];
            const vic = atk === WX[wa] ? WX[wb] : WX[wa];
            applyKe(out, atk, vic, e, sk);
            kinds.push("ke");
          } else if (shengDir(wa, wb) || shengDir(wb, wa)) {
            const gen = shengDir(wa, wb) ? WX[wa] : WX[wb];
            const recv = gen === WX[wa] ? WX[wb] : WX[wa];
            applyShengMin(out, gen, recv, e, sk);
            kinds.push("sheng");
          }
        }
        kinds.push("ban");
      }
    } else if (BANHE[pairKey(brA, brB)] || BANHE[brA + brB] || BANHE[brB + brA]) {
      hua = BANHE[brA + brB] || BANHE[brB + brA] || BANHE[pairKey(brA, brB)];
      const zhong = ZHONG_SHEN[hua];
      if (zhong && (brA === zhong || brB === zhong || zhiSet.indexOf(zhong) >= 0)) {
        openVault(critCtx, brA);
        openVault(critCtx, brB);
        applyHuaLiangAdd(brA, brB, hua, e, critCtx);
        kinds.push("huajia");
      }
    }
    // 会合：合之后冲→生→克。字内／同行不作用。a>0 只合冲。
    mergeHideKinds(kinds, applyHideHeChongShengKe(
      hidesOf(brA, critCtx), hidesOf(brB, critCtx), e, sk, critCtx, lossFn, gainFn, out,
      null, null, { skipTonghang: true, a: flags.a, cls: "hehui" }
    ));
    return kinds;
  }

  function applyXingChongPair(brA, brB, e, sk, critCtx, lossFn, gainFn, out, flags) {
    flags = flags || {};
    const types = zhiPairSpecialTypes(brA, brB);
    const kinds = [];
    critCtx = critCtx || {};
    if (types.indexOf("xing") >= 0 || types.indexOf("chong") >= 0) {
      const oa = openVault(critCtx, brA);
      const ob = openVault(critCtx, brB);
      if (oa || ob) kinds.push("kaiku");
    }
    // 冲刑害破：冲→合→克→生。害不开库。a>0 只冲合。
    mergeHideKinds(kinds, applyHideHeChongShengKe(
      hidesOf(brA, critCtx), hidesOf(brB, critCtx), e, sk, critCtx, lossFn, gainFn, out,
      null, null, { skipTonghang: true, a: flags.a, cls: "xingchong" }
    ));
    return kinds;
  }

  /**
   * 地支特关：会合合→冲→生→克；冲刑害破冲→合→克→生。不算同行。
   */
  function applyZhiSpecialHideEnergy(brA, brB, e, sk, critCtx, lossFn, gainFn, out, flags) {
    const zhiSet = (critCtx && critCtx.zhiSet) || [brA, brB];
    const cls = zhiSpecialClass(brA, brB, zhiSet);
    if (cls === "hehui") return applyHeHuiPair(brA, brB, e, sk, critCtx, lossFn, gainFn, out, flags);
    if (cls === "xingchong") return applyXingChongPair(brA, brB, e, sk, critCtx, lossFn, gainFn, out, flags);
    return [];
  }

  // 五行生克方向：wxA 生 wxB ?
  function shengDir(wxA, wxB) { return (wxA + 1) % 5 === wxB; }
  function keDir(wxA, wxB) { return (wxA + 2) % 5 === wxB; }

  /**
   * 克能量互减（已锁定）。A 克 B。
   * A＞B：a=A−(1/8)B，b=B−(3/8)B
   * A＜B：a=A−(1/8)A，b=B−(3/8)A
   * A＝B 两式同值。再 ×E（紧贴 E=1 即上式）。
   */
  function keMutualDelta(A, B, e, sk) {
    const atkFrac = Math.abs(sk && sk.ke && sk.ke.a != null ? sk.ke.a : 0.125);
    const vicFrac = Math.abs(sk && sk.ke && sk.ke.b != null ? sk.ke.b : 0.375);
    const ref = Math.min(Number(A) || 0, Number(B) || 0);
    const ee = e == null ? 1 : e;
    return {
      a: Math.max(0, (Number(A) || 0) - atkFrac * ref * ee),
      b: Math.max(0, (Number(B) || 0) - vicFrac * ref * ee),
      ref: ref,
    };
  }

  function applyKe(out, atkWxName, vicWxName, e, sk) {
    const d = keMutualDelta(out[atkWxName] || 0, out[vicWxName] || 0, e, sk);
    out[atkWxName] = d.a;
    out[vicWxName] = d.b;
    return d;
  }

  const IMASK = {
    sanhui: 3.0, sanhe: 2.5, sanxing: 2.5, liuhe: 2.0, chong: 2.0, sichong: 2.0,
    wuhe: 2.0, banhe: 1.5, xing: 1.5, hai: 1.25, po: 1.0,
    sheng: 1.0, ke: 1.0, tongzi: 1.0, tonghang: 1.0, tonggen: 1.0, genqi: 1.0,
  };

  /** 天干透出集合 */
  function natalTou(pillars) {
    const set = {};
    PKEY.forEach(k => { const gz = pillars[k]; if (gz && gz[0]) set[gz[0]] = true; });
    return set;
  }

  /** 通根：天干 → [{branch, pillar, weight}] */
  function stemRootsOf(pillars) {
    const roots = {};
    PKEY.forEach((k, pi) => {
      const gz = pillars[k]; if (!gz) return;
      (HIDE[gz[1]] || []).forEach(([hs, w]) => {
        (roots[hs] = roots[hs] || []).push({ branch: gz[1], pillar: PZH[k], pi: pi, weight: w });
      });
    });
    return roots;
  }

  /** 两透干同根于同一地支 → 根气共振（隔位亦可互通） */
  function sharedRootPairs(pillars) {
    const tou = natalTou(pillars);
    const roots = stemRootsOf(pillars);
    const stems = Object.keys(roots).filter(s => tou[s]);
    const pairs = [];
    for (let i = 0; i < stems.length; i++) {
      for (let j = i + 1; j < stems.length; j++) {
        const A = stems[i], B = stems[j];
        const shared = (roots[A] || []).filter(r => (roots[B] || []).some(s => s.branch === r.branch));
        if (shared.length) {
          pairs.push({
            stems: [A, B], branch: shared[0].branch, pillar: shared[0].pillar, weight: shared[0].weight,
            note: `${A}与${B}同根于${shared[0].pillar}支${shared[0].branch}，隔位亦能量互通`,
          });
        }
      }
    }
    return pairs;
  }

  /** 库中藏干不透 → 永不作向外用神（含合身） */
  function vaultTrappedOf(pillars) {
    const tou = natalTou(pillars);
    const out = [];
    PKEY.forEach(k => {
      const gz = pillars[k]; if (!gz) return;
      const br = gz[1];
      if (!VAULT[br]) return;
      (HIDE[br] || []).forEach(([hs, w]) => {
        if (!tou[hs]) {
          out.push({
            branch: br, stem: hs, weight: w, pillar: PZH[k],
            note: `${PZH[k]}支${br}中${hs}不透，库中物不出，永不作向外用神／合身`,
          });
        }
      });
    });
    return out;
  }

  function genqiPairOf(A, B, pairs) {
    if (!pairs || !A || !B) return null;
    for (let i = 0; i < pairs.length; i++) {
      const p = pairs[i];
      const s = p.stems || [];
      if ((s[0] === A.ch && s[1] === B.ch) || (s[0] === B.ch && s[1] === A.ch)) return p;
    }
    return null;
  }

  /**
   * 杀印同根：不走克身，走化杀生身。
   * 杀干克日主、杀根支克日主（如申克甲）一律跳过。
   */
  function skipShaKeViaRoot(A, B, ctx) {
    if (!ctx || !ctx.sharedPairs || !ctx.dayStem) return false;
    const day = ctx.dayStem;
    const dayWx = stemWX(day);
    let killer = null;
    if (A.ch === day && keDir(B.wx, A.wx)) killer = B;
    else if (B.ch === day && keDir(A.wx, B.wx)) killer = A;
    if (!killer) return false;
    return ctx.sharedPairs.some(p => {
      const sha = (p.stems || []).find(s => keDir(stemWX(s), dayWx));
      const yin = (p.stems || []).find(s => s !== sha);
      if (!sha || !yin || !shengDir(stemWX(yin), dayWx)) return false;
      return killer.ch === sha || killer.ch === p.branch;
    });
  }

  const SHI_SHEN = {
    甲: { 甲:"比肩", 乙:"劫财", 丙:"食神", 丁:"伤官", 戊:"偏财", 己:"正财", 庚:"七杀", 辛:"正官", 壬:"偏印", 癸:"正印" },
    乙: { 乙:"比肩", 甲:"劫财", 丁:"食神", 丙:"伤官", 己:"偏财", 戊:"正财", 辛:"七杀", 庚:"正官", 癸:"偏印", 壬:"正印" },
    丙: { 丙:"比肩", 丁:"劫财", 戊:"食神", 己:"伤官", 庚:"偏财", 辛:"正财", 壬:"七杀", 癸:"正官", 甲:"偏印", 乙:"正印" },
    丁: { 丁:"比肩", 丙:"劫财", 己:"食神", 戊:"伤官", 辛:"偏财", 庚:"正财", 癸:"七杀", 壬:"正官", 乙:"偏印", 甲:"正印" },
    戊: { 戊:"比肩", 己:"劫财", 庚:"食神", 辛:"伤官", 壬:"偏财", 癸:"正财", 甲:"七杀", 乙:"正官", 丙:"偏印", 丁:"正印" },
    己: { 己:"比肩", 戊:"劫财", 辛:"食神", 庚:"伤官", 癸:"偏财", 壬:"正财", 乙:"七杀", 甲:"正官", 丁:"偏印", 丙:"正印" },
    庚: { 庚:"比肩", 辛:"劫财", 壬:"食神", 癸:"伤官", 甲:"偏财", 乙:"正财", 丙:"七杀", 丁:"正官", 戊:"偏印", 己:"正印" },
    辛: { 辛:"比肩", 庚:"劫财", 癸:"食神", 壬:"伤官", 乙:"偏财", 甲:"正财", 丁:"七杀", 丙:"正官", 己:"偏印", 戊:"正印" },
    壬: { 壬:"比肩", 癸:"劫财", 甲:"食神", 乙:"伤官", 丙:"偏财", 丁:"正财", 戊:"七杀", 己:"正官", 庚:"偏印", 辛:"正印" },
    癸: { 癸:"比肩", 壬:"劫财", 乙:"食神", 甲:"伤官", 丁:"偏财", 丙:"正财", 己:"七杀", 戊:"正官", 辛:"偏印", 庚:"正印" },
  };

  function shiShenOf(day, ch) {
    return (SHI_SHEN[day] && SHI_SHEN[day][ch]) || "";
  }

  /**
   * 通根：同天干藏干字共振 + 同柱同行传递。同五行只传递、不生克。
   * 生链键（同柱干支生克）只建键，不改能量。
   */
  function applyTongGenAndChain(chars, pillars, schoolCtx, push) {
    const ganList = chars.filter(function (c) { return c.layer === "gan" && !c.global; });
    ganList.forEach(function (g) {
      PKEY.forEach(function (k, pi) {
        const gz = pillars[k]; if (!gz) return;
        hidesOf(gz[1]).forEach(function (h) {
          if (h.ch === g.ch) {
            push({
              type: "tonggen", cls: relCls("tonggen"), chars: [g.ch, gz[1]],
              a: natalDist({ pillar: g.pillar, layer: "gan" }, { pillar: pi, layer: "zhi" }),
              layerZh: `${g.lay}干↔${PZH[k]}支`,
              note: `${g.ch}通根${PZH[k]}支${gz[1]}藏${h.ch}·同字共振`,
            });
          }
          if (g.pillar === pi && h.wx === g.wx) {
            push({
              type: "tongzi", cls: relCls("tongzi"), chars: [g.ch, h.ch],
              a: 0, layerZh: `${g.lay}干↔${PZH[k]}支`,
              note: `${g.ch}${h.ch}·同柱同行传递`,
            });
          }
        });
      });
    });
    PKEY.forEach(function (k, pi) {
      const gan = chars.find(function (c) { return c.layer === "gan" && c.pillar === pi && !c.global; });
      const zhi = chars.find(function (c) { return c.layer === "zhi" && c.pillar === pi && !c.global; });
      if (!gan || !zhi) return;
      if (gan.wx === zhi.wx) return;
      if (shengDir(gan.wx, zhi.wx) || shengDir(zhi.wx, gan.wx)) {
        push({
          type: "sheng", cls: relCls("sheng"), chars: [gan.ch, zhi.ch],
          a: 0, layerZh: `${gan.lay}干↔${zhi.lay}支`,
          note: `${gan.ch}${zhi.ch}·生链·同柱生（能量已定）`,
          chainOnly: true,
        });
      } else if (keDir(gan.wx, zhi.wx) || keDir(zhi.wx, gan.wx)) {
        if (skipShaKeViaRoot(gan, zhi, schoolCtx)) {
          push({
            type: "genqi", cls: relCls("genqi"), chars: [zhi.ch, gan.ch],
            a: 0, layerZh: `${zhi.lay}支↔${gan.lay}干`,
            note: `${zhi.ch}${gan.ch}·杀根不克身（化杀生身）`,
            chainOnly: true,
          });
        } else {
          const zhiAtk = keDir(zhi.wx, gan.wx);
          push({
            type: "ke", cls: relCls("ke"),
            chars: zhiAtk ? [zhi.ch, gan.ch] : [gan.ch, zhi.ch],
            a: 0, layerZh: `${gan.lay}干↔${zhi.lay}支`,
            note: `${zhi.ch}${gan.ch}·生链·同柱克（能量已定）`,
            chainOnly: true,
          });
        }
      }
    });
  }

  /** 目字十神生链／截断：能量已定，只判流通与截断。地支内藏干不互作用。 */
  function buildMuZiShengChain(pillars, dayStem) {
    const day = dayStem || (pillars.day && pillars.day[0]) || "";
    const seats = [];
    PKEY.forEach(function (k) {
      const gz = pillars[k]; if (!gz) return;
      seats.push({ layer: "gan", ch: gz[0], wx: stemWX(gz[0]), seat: PZH[k] + "干", ss: shiShenOf(day, gz[0]), w: 1 });
      hidesOf(gz[1]).forEach(function (h) {
        seats.push({ layer: "hide", ch: h.ch, wx: h.wx, seat: PZH[k] + "支" + gz[1], ss: shiShenOf(day, h.ch), w: h.w, branch: gz[1] });
      });
    });
    const supports = [];
    const cuts = [];
    const gans = seats.filter(function (s) { return s.layer === "gan"; });
    const hides = seats.filter(function (s) { return s.layer === "hide"; });
    gans.forEach(function (g) {
      hides.forEach(function (h) {
        if (g.wx === h.wx) {
          supports.push({ kind: "transfer", from: h.seat + h.ch, to: g.seat + g.ch, ss: h.ss, note: "同行传递" });
          return;
        }
        if (shengDir(h.wx, g.wx) || shengDir(g.wx, h.wx)) {
          const gen = shengDir(h.wx, g.wx) ? h : g;
          const recv = gen === h ? g : h;
          supports.push({ kind: "sheng", from: gen.seat + gen.ch, to: recv.seat + recv.ch, ss: gen.ss + "→" + recv.ss, note: "生链" });
        } else if (keDir(h.wx, g.wx) || keDir(g.wx, h.wx)) {
          const atk = keDir(h.wx, g.wx) ? h : g;
          const vic = atk === h ? g : h;
          cuts.push({ kind: "ke", from: atk.seat + atk.ch, to: vic.seat + vic.ch, ss: atk.ss + "克" + vic.ss, note: "截断" });
        }
      });
    });
    return {
      day: day,
      seats: seats,
      supports: supports,
      cuts: cuts,
      frozen: true,
      note: "目字十神生链／截断：能量已定，仅作流通与截断的判断。地支内五行不作用。",
    };
  }

  /**
   * 扫描四柱，产出键列表 + 生克后能量态 + 烈度。
   * @param pillars {year,month,day,time} 干支字串
   * @param el 令地后五行 totals（可选；用于生克后损益）
   * @param P KRParams（2026-08-11）
   */
  function scan(pillars, el, P, extra) {
    P = P || global.KRParams || {};
    const sk = P.shengKe || {};
    const chars = [];
    PKEY.forEach((k, i) => {
      const gz = pillars[k]; if (!gz || gz.length < 2) return;
      chars.push({ layer: "gan", ch: gz[0], pillar: i, wx: stemWX(gz[0]), lay: PZH[k] });
      chars.push({ layer: "zhi", ch: gz[1], pillar: i, wx: branchWX(gz[1]), lay: PZH[k] });
    });
    // 动态柱（大运/流年/流月/流日/流时/风水）：全局作用（键距 E=1，可及原局每字）
    (extra || []).forEach((dp, di) => {
      const gz = dp.gz; if (!gz || gz.length < 2) return;
      const pidx = 4 + di;
      chars.push({ layer: "gan", ch: gz[0], pillar: pidx, wx: stemWX(gz[0]), lay: dp.label || ("动" + di), global: true });
      chars.push({ layer: "zhi", ch: gz[1], pillar: pidx, wx: branchWX(gz[1]), lay: dp.label || ("动" + di), global: true });
    });
    const charByBranch = {};
    chars.filter(c => c.layer === "zhi").forEach(c => { (charByBranch[c.ch] = charByBranch[c.ch] || []).push(c.lay); });
    const laysOf = (branch) => (charByBranch[branch] || []).join("+");

    const keys = [];
    const transfers = [];
    const out = Object.assign({ 木:0,火:0,土:0,金:0,水:0 }, el || {});
    function loss(wxName, unit, e) { if (out[wxName] != null) out[wxName] *= (1 + unit * e); }
    function gain(wxName, unit, e) { if (out[wxName] != null) out[wxName] *= (1 + unit * e); }

    // counts for intensity ×同键数量
    const counts = {};
    function bump(type) { counts[type] = (counts[type] || 0) + 1; }

    function push(k) {
      const base = IMASK[k.type] || 1.0;
      k.intensity = +(base * E(k.a != null ? k.a : 0)).toFixed(3);
      k.manifest = k.intensity >= 2.0;
      keys.push(k);
      bump(k.type);
    }

    // —— 三字层：三会 / 三合 / 三刑 ——（地支）
    const zhiList = chars.filter(c => c.layer === "zhi");
    const zhiSet = zhiList.map(c => c.ch);
    function has(ch) { return zhiSet.includes(ch); }
    SANHUI.forEach(g => { if (has(g[0]) && has(g[1]) && has(g[2])) push({ type:"sanhui", cls:"三会方", chars:g.slice(0,3), hua:g[3], a:0, layers:g.slice(0,3).map(laysOf), note:`${g.slice(0,3).join("")}会${g[3]}（${g.slice(0,3).map(b=>b+laysOf(b)).join("·")}）` }); });
    SANHE.forEach(g => { if (has(g[0]) && has(g[1]) && has(g[2])) { push({ type:"sanhe", cls:"三合局", chars:g.slice(0,3), hua:g[3], a:0, layers:g.slice(0,3).map(laysOf), note:`${g.slice(0,3).join("")}合${g[3]}局（${g.slice(0,3).map(b=>b+laysOf(b)).join("·")}；拆两字藏干四冲五合）` }); } });
    XING_GROUPS.forEach(g => { if (has(g[0]) && has(g[1]) && has(g[2])) push({ type:"sanxing", cls:"三刑", chars:g.slice(), a:0, layers:g.map(laysOf), note:`${g.join("")}三刑` }); });

    // —— 两字层：先特关（刑开库只冲／害只冲／半合火量相加）再普通生克 ——
    zhiList.sort(function (p, q) { return (p.pillar - q.pillar) || String(p.lay).localeCompare(String(q.lay)); });
    const openedVaults = {};
    function runZhiPass(zhiPass) {
      for (let i = 0; i < zhiList.length; i++) {
        for (let j = i + 1; j < zhiList.length; j++) {
          const A = zhiList[i], B = zhiList[j];
          const special = isZhiSpecialPair(A.ch, B.ch, zhiSet);
          if (special && zhiPass !== "special") continue;
          if (!special && zhiPass !== "ordinary") continue;
          const a = natalDist(A, B);
          const e = E(a);
          const rels = [];
          if (A.ch === B.ch) {
            rels.push("tongzi");
            applyTongziBoost(A, B, e, out, transfers);
          }
          zhiPairSpecialTypes(A.ch, B.ch).forEach(function (t) { if (rels.indexOf(t) < 0) rels.push(t); });
          const critZ = {
            monthBr: pillars.month && pillars.month[1],
            pillars: pillars,
            keyA: LAY_KEY[A.lay] || "",
            keyB: LAY_KEY[B.lay] || "",
            transfers: transfers,
            zhiSet: zhiSet,
            openedVaults: openedVaults,
          };
          if (special) {
            applyZhiSpecialHideEnergy(A.ch, B.ch, e, sk, critZ, loss, gain, out, { a: a });
          } else if (isTightShengKe(A, B, a)) {
            applyHideHeChongShengKe(hidesOf(A.ch, critZ), hidesOf(B.ch, critZ), 1, sk, critZ, loss, gain, out, A.wx, B.wx, { skipTonghang: true }).forEach(function (t) {
              if (rels.indexOf(t) < 0) rels.push(t);
            });
            if (keDir(A.wx, B.wx) || keDir(B.wx, A.wx)) {
              if (rels.indexOf("ke") < 0) rels.push("ke");
            } else if (shengDir(A.wx, B.wx) || shengDir(B.wx, A.wx)) {
              if (rels.indexOf("sheng") < 0) rels.push("sheng");
            }
          }
          rels.forEach(function (t) {
            push({
              type: t, cls: relCls(t), chars: [A.ch, B.ch],
              layerZh: `${A.lay}支↔${B.lay}支`,
              a, doubleKey: rels.length > 1,
              note: `${A.ch}${B.ch}·${relCls(t)}${rels.length > 1 ? "（双键）" : ""}`,
            });
          });
        }
      }
    }
    runZhiPass("special");
    runZhiPass("ordinary");

    // —— 天干层：五合／四冲可隔位遥感×E；普通生克隔位归零；杀印同根不走克身 ——
    const ganList = chars.filter(c => c.layer === "gan");
    const heStems = collectGanHeStems(ganList);
    const schoolCtx = {
      dayStem: pillars.day ? pillars.day[0] : "",
      sharedPairs: sharedRootPairs(pillars),
    };
    for (let i = 0; i < ganList.length; i++) {
      for (let j = i + 1; j < ganList.length; j++) {
        const A = ganList[i], B = ganList[j];
        const a = natalDist(A, B);
        const e = E(a);
        const rels = [];
        if (A.ch === B.ch) {
          rels.push("tongzi");
          applyTongziBoost(A, B, e, out, transfers);
        }
        if (TIANGAN_HE[A.ch] === B.ch) {
          rels.push("wuhe");
          const hua = lookupHua(TIANGAN_HE_HUA, A.ch, B.ch);
          const monthBr = pillars.month && pillars.month[1];
          const crit = hua ? hehuaThreshold(hua, monthBr, pillars, A, B) : { canHua: false, mode: "ban" };
          applyWuHeEnergy(loss, gain, A, B, e, sk, crit, out);
        }
        let chongResolved = false;
        if (TIANGAN_CHONG[A.ch] === B.ch) {
          chongResolved = !!(heStems[A.ch] || heStems[B.ch]);
          rels.push("sichong");
          if (!chongResolved) applySiChongEnergy(loss, A, B, e, sk, out);
        }
        const skipSha = skipShaKeViaRoot(A, B, schoolCtx);
        if (!rels.length && isTightShengKe(A, B, a)) {
          if (skipSha && (keDir(A.wx, B.wx) || keDir(B.wx, A.wx))) {
            rels.push("genqi");
          } else {
            applyHideHeChongShengKe([stemAsHide(A.ch)], [stemAsHide(B.ch)], 1, sk, {
              monthBr: pillars.month && pillars.month[1], pillars: pillars, keyA: A, keyB: B, transfers: transfers,
            }, loss, gain, out, A.wx, B.wx).forEach(function (t) {
              if (rels.indexOf(t) < 0) rels.push(t);
            });
          }
        } else if (isTightShengKe(A, B, a) && A.wx === B.wx && rels.indexOf("wuhe") < 0 && rels.indexOf("sichong") < 0) {
          if (rels.indexOf("tonghang") < 0) rels.push("tonghang");
          recordTonghang({ transfers: transfers }, A, B, 1, "天干同行传递");
        }
        rels.forEach(t => push({
          type: t, cls: relCls(t), chars:[A.ch, B.ch],
          layerZh: `${A.lay}干↔${B.lay}干`,
          a, doubleKey: rels.length > 1,
          note: `${A.ch}${B.ch}·${relCls(t)}${t === "sichong" && chongResolved ? "（五合解冲）" : ""}`,
          resolved: t === "sichong" ? chongResolved : undefined,
        }));
      }
    }

    const energyGanZhi = { 木: out.木, 火: out.火, 土: out.土, 金: out.金, 水: out.水 };

    // —— 通根（干支作用）：同天干藏干字共振 + 同柱同行传递；生链只建键 ——
    applyTongGenAndChain(chars, pillars, schoolCtx, push);

    // —— 通根清单（天干本气/中余根于地支藏干）——
    const tonggen = [];
    ganList.forEach(g => {
      PKEY.forEach((k, pi) => {
        const gz = pillars[k]; if (!gz) return;
        const hides = HIDE[gz[1]] || [];
        hides.forEach(([hs, w]) => {
          if (hs === g.ch) tonggen.push({ stem: g.ch, atPillar: PZH[k], weight: w, self: pi === g.pillar,
            note: `${g.ch}通根${PZH[PKEY[g.pillar]]}干→${PZH[k]}支(${gz[1]}藏${hs}·${w})` });
        });
      });
    });

    schoolCtx.sharedPairs.forEach(p => {
      push({ type:"genqi", cls:relCls("genqi"), chars:p.stems.slice(), a:0,
        layerZh:`同根${p.pillar}支${p.branch}`, note:p.note });
    });
    const vaultTrapped = vaultTrappedOf(pillars);

    // 同键数量 ×：把同类键的烈度按数量加权（canon：×同键数量，双键不另×2）
    keys.forEach(k => { const n = counts[k.type] || 1; if (n > 1) k.intensity = +(k.intensity * (1 + 0.2 * (n - 1))).toFixed(3); k.manifest = k.intensity >= 2.0; });

    const manifest = keys.filter(k => k.manifest);
    const maxIntensity = keys.reduce((m, k) => Math.max(m, k.intensity), 0);

    const shengChain = buildMuZiShengChain(pillars, schoolCtx.dayStem);
    return { keys, tonggen, energyAfter: out, energyGanZhi, shengChain, counts, manifest, maxIntensity,
      sharedRoots: schoolCtx.sharedPairs, vaultTrapped, transfers };
  }

  function relCls(t) {
    return { liuhe:"六合", banhe:"半合", chong:"冲", hai:"害", po:"破", xing:"刑",
      sheng:"生", ke:"克", tongzi:"同字共振", tonghang:"同行传递", wuhe:"天干五合", sichong:"天干四冲",
      sanhui:"三会", sanhe:"三合", sanxing:"三刑", tonggen:"通根", genqi:"根气共振" }[t] || t;
  }

  const ENTANGLE = { liuhe:1, banhe:1, chong:1, sichong:1, hai:1, po:1, xing:1, wuhe:1, sanhui:1, sanhe:1, sanxing:1, tongzi:1, genqi:1 };
  const HUA_WX = { 木:0, 火:1, 土:2, 金:3, 水:4 };

  function hideWeightForHua(branch, hua) {
    const hides = HIDE[branch] || [];
    const hwx = HUA_WX[hua];
    let sum = 0;
    hides.forEach(([st, w]) => {
      const sw = stemWX(st);
      if (sw === hwx) sum += w;
      else if (shengDir(sw, hwx)) sum += w; // 印星捧献
    });
    if (branch === "巳") {
      if (hua === "火") return 0.6;
      if (hua === "金") return 0.3;
      if (hua === "土") return 0.1;
    }
    return sum;
  }
  function tripleAB(g, kind) {
    const hua = g[3], brs = g.slice(0, 3);
    const nonVault = brs.filter(b => !VAULT[b]);
    const vaults = brs.filter(b => VAULT[b]);
    if (kind === "sanhui") {
      const same = nonVault.filter(b => branchWX(b) === HUA_WX[hua]);
      const ab = same.length >= 2 ? 1.0 + 1.0 : hideWeightForHua(nonVault[0] || brs[0], hua) + hideWeightForHua(nonVault[1] || brs[1], hua);
      return { ab: +ab.toFixed(3), vault: vaults[0] || "", c: 0.5 };
    }
    const a = hideWeightForHua(nonVault[0] || brs[0], hua);
    const b = hideWeightForHua(nonVault[1] || brs[1], hua);
    return { ab: +(a + b).toFixed(3), vault: vaults[0] || "", c: 0.5 };
  }
  function inWindow(ab, kind) {
    if (kind === "sanhui") return ab > 1.8 && ab < 2.2;
    if (kind === "sanhe") return ab > 1.5 && ab < 1.9;
    return true;
  }

  function flowLayerIndex(label, fallback) {
    const map = { "流年": 0, "流月": 1, "流日": 2, "流时": 3 };
    if (label && Object.prototype.hasOwnProperty.call(map, label)) return map[label];
    if (global.KREngine && global.KREngine.flowLayerIndex) {
      return global.KREngine.flowLayerIndex(label, fallback);
    }
    return fallback == null ? 0 : fallback;
  }

  function makeChars(pillars, extra) {
    const chars = [];
    PKEY.forEach((k, i) => {
      const gz = pillars[k]; if (!gz || gz.length < 2) return;
      if (TG.indexOf(gz[0]) < 0 || DZ.indexOf(gz[1]) < 0) return;
      chars.push({ layer: "gan", ch: gz[0], pillar: i, wx: stemWX(gz[0]), lay: PZH[k], kind: "natal", scale: 1 });
      chars.push({ layer: "zhi", ch: gz[1], pillar: i, wx: branchWX(gz[1]), lay: PZH[k], kind: "natal", scale: 1 });
    });
    (extra || []).forEach((dp, di) => {
      const gz = dp.gz; if (!gz || gz.length < 2) return;
      const kind = dp.kind || (dp.label && /流/.test(dp.label) ? "flow" : "yun");
      const scale = dp.scale != null ? dp.scale : (dp.label === "小运" ? 0.125 : 1);
      const flowIdx = dp.flowIdx != null ? dp.flowIdx : flowLayerIndex(dp.label, di);
      const pidx = kind === "flow" ? flowIdx : (4 + di);
      chars.push({ layer: "gan", ch: gz[0], pillar: pidx, wx: stemWX(gz[0]), lay: dp.label || ("动" + di), kind: kind, scale: scale });
      chars.push({ layer: "zhi", ch: gz[1], pillar: pidx, wx: branchWX(gz[1]), lay: dp.label || ("动" + di), kind: kind, scale: scale });
    });
    return chars;
  }

  function pairDist(A, B) {
    if (!A || !B) return 99;
    if (A.kind === "natal" && B.kind === "natal") return natalDist(A, B);
    if (A.kind === "flow" && B.kind === "flow") return Math.abs((A.pillar || 0) - (B.pillar || 0));
    const yun = A.kind === "yun" ? A : (B.kind === "yun" ? B : null);
    const natal = A.kind === "natal" ? A : (B.kind === "natal" ? B : null);
    if (yun && natal) {
      const p = natal.pillar;
      if (isXiaoYun(yun)) {
        if (p === 2 || p === 3) return 0;
        if (p === 1) return 1;
        if (p === 0) return 2;
        return 0;
      }
      return p === 3 ? 1 : 0;
    }
    if (yun && A.kind === "yun" && B.kind === "yun") return 0;
    const flow = A.kind === "flow" ? A : (B.kind === "flow" ? B : null);
    const yun2 = A.kind === "yun" ? A : (B.kind === "yun" ? B : null);
    if (flow && yun2) return flow.pillar == null ? 0 : flow.pillar;
    if (flow) return flow.pillar == null ? 0 : flow.pillar;
    return 1;
  }
  function isFlow(c) { return !!(c && c.kind === "flow"); }
  function isNatalChar(c) { return !!(c && c.kind === "natal"); }
  function isYunChar(c) { return !!(c && c.kind === "yun"); }
  function isNatalYun(c) { return isNatalChar(c) || isYunChar(c); }
  /** 流层与原局分隔：不生克。入局只经特关纠缠与共振。 */
  function flowNatalSeparated(A, B) { return (isFlow(A) && isNatalChar(B)) || (isFlow(B) && isNatalChar(A)); }
  function noBridgeKe(A, B) { return flowNatalSeparated(A, B); }

  function applyPair(A, B, ctx) {
    const a = pairDist(A, B);
    const scale = Math.min(A.scale || 1, B.scale || 1);
    const e = E(a) * scale;
    const sk = ctx.sk;
    const pk = pairKey(A.ch, B.ch);
    const rels = [];
    ctx._chongResolved = false;
    const bothZhi = A.layer === "zhi" && B.layer === "zhi";
    const bothGan = A.layer === "gan" && B.layer === "gan";
    if (A.ch === B.ch) {
      rels.push("tongzi");
      applyTongziBoost(A, B, e, ctx.out, ctx.transfers);
    }
    if (bothZhi) {
      zhiPairSpecialTypes(A.ch, B.ch).forEach(function (t) { if (rels.indexOf(t) < 0) rels.push(t); });
      const special = isZhiSpecialPair(A.ch, B.ch, ctx.zhiSet);
      const critZ = {
        monthBr: ctx.monthBr,
        pillars: ctx.pillars,
        keyA: LAY_KEY[A.lay] || "",
        keyB: LAY_KEY[B.lay] || "",
        transfers: ctx.transfers,
        zhiSet: ctx.zhiSet,
        openedVaults: ctx.openedVaults,
      };
      if (special && ctx.zhiPass && ctx.zhiPass !== "special") return rels;
      if (!special && ctx.zhiPass && ctx.zhiPass !== "ordinary") return rels;
      if (special) {
        applyZhiSpecialHideEnergy(A.ch, B.ch, e, sk, critZ, ctx.loss, ctx.gain, ctx.out, { a: a });
      } else if (isTightShengKe(A, B, a) && !ctx.skipShengKe) {
        applyHideHeChongShengKe(hidesOf(A.ch, critZ), hidesOf(B.ch, critZ), 1, sk, critZ, ctx.loss, ctx.gain, ctx.out, A.wx, B.wx, { skipTonghang: true }).forEach(function (t) {
          if (rels.indexOf(t) < 0) rels.push(t);
        });
        if (keDir(A.wx, B.wx) || keDir(B.wx, A.wx)) {
          if (rels.indexOf("ke") < 0) rels.push("ke");
        } else if (shengDir(A.wx, B.wx) || shengDir(B.wx, A.wx)) {
          if (rels.indexOf("sheng") < 0) rels.push("sheng");
        }
      }
    }
    if (bothGan && TIANGAN_HE[A.ch] === B.ch) {
      rels.push("wuhe");
      const hua = lookupHua(TIANGAN_HE_HUA, A.ch, B.ch);
      const crit = hua ? hehuaThreshold(hua, ctx.monthBr, ctx.pillars, A, B) : { canHua: false, mode: "ban" };
      applyWuHeEnergy(ctx.loss, ctx.gain, A, B, e, sk, crit, ctx.out);
    }
    if (bothGan && TIANGAN_CHONG[A.ch] === B.ch) {
      const resolved = !!(ctx.heStems && (ctx.heStems[A.ch] || ctx.heStems[B.ch]));
      rels.push("sichong");
      if (!resolved) applySiChongEnergy(ctx.loss, A, B, e, sk, ctx.out);
      ctx._chongResolved = resolved;
    }
    const zhiSpecial = bothZhi && isZhiSpecialPair(A.ch, B.ch, ctx.zhiSet);
    if (bothGan && (!rels.length || (rels.length === 1 && rels[0] === "tongzi")) && !zhiSpecial) {
      const skipSK = ctx.skipShengKe || flowNatalSeparated(A, B);
      const skipSha = skipShaKeViaRoot(A, B, ctx);
      if (!skipSK && isTightShengKe(A, B, a)) {
        if (skipSha && (keDir(A.wx, B.wx) || keDir(B.wx, A.wx))) {
          rels.push("genqi");
        } else {
          applyHideHeChongShengKe([stemAsHide(A.ch)], [stemAsHide(B.ch)], 1, sk, {
            monthBr: ctx.monthBr, pillars: ctx.pillars, keyA: A, keyB: B, transfers: ctx.transfers,
          }, ctx.loss, ctx.gain, ctx.out, A.wx, B.wx).forEach(function (t) {
            if (rels.indexOf(t) < 0) rels.push(t);
          });
        }
      }
    } else if (flowNatalSeparated(A, B)) {
      // 流对原局：只特关纠缠／单字共振入局，不追加直克
    }
    rels.forEach(t => {
      if (flowNatalSeparated(A, B) && (t === "sheng" || t === "ke")) return;
      const sameGZ = A.layer !== B.layer && A.pillar === B.pillar && A.kind === B.kind;
      const rk = resonateKind(t);
      const flowed = rk === "entangle" || rk === "shengke";
      const sep = flowNatalSeparated(A, B);
      const vsYun = (isFlow(A) && isYunChar(B)) || (isFlow(B) && isYunChar(A));
      let tag = "";
      if (t === "tongzi") tag = "（单字共振·不流动）";
      else if (sep && flowed) tag = "（纠缠共振入局）";
      else if (vsYun && rk === "shengke") tag = "（生克共振）";
      else if (sep) tag = "（流原分隔）";
      ctx.push({
        type: t, cls: relCls(t), chars: [A.ch, B.ch],
        layerZh: `${A.lay}${A.layer === "gan" ? "干" : "支"}↔${B.lay}${B.layer === "gan" ? "干" : "支"}`,
        a, doubleKey: rels.length > 1, o5: sep, resonate: rk, flowed: flowed,
        note: `${A.ch}${B.ch}·${relCls(t)}${sameGZ ? "（同柱）" : ""}${tag}${t === "sichong" && ctx._chongResolved ? "（五合解冲）" : ""}`,
        resolved: t === "sichong" ? !!ctx._chongResolved : undefined,
      });
    });
    return rels;
  }

  /**
   * 柱序流水线（O18）：年→月→日→时→大运→小运→流年→流月→流日→流时
   * O5：流层对原局分隔不生克，只经纠缠／单字共振入局；流年对运 a=0 可生克
   * O7：三会 1.8＜A+B＜2.2／三合 1.5＜A+B＜1.9 成键；缺一或未入窗→两字生克；成键仍拆两字
   */
  function pipeline(pillars, el, P, extra) {
    P = P || global.KRParams || {};
    const sk = P.shengKe || {};
    const extras = extra || [];
    const natalEx = extras.filter(d => (d.kind || (/流/.test(d.label || "") ? "flow" : "yun")) !== "flow");
    const flowEx = extras.filter(d => (d.kind || (/流/.test(d.label || "") ? "flow" : "yun")) === "flow")
      .map((d, i) => Object.assign({}, d, {
        kind: "flow",
        flowIdx: d.flowIdx != null ? d.flowIdx : flowLayerIndex(d.label, i),
      }));
    const yunEx = natalEx.map((d, i) => Object.assign({}, d, { kind: d.kind || "yun", scale: d.scale != null ? d.scale : (d.label === "小运" ? 0.125 : 1) }));

    const natalChars = makeChars(pillars, []);
    const yunChars = makeChars({ year: "  ", month: "  ", day: "  ", time: "  " }, yunEx).filter(c => c.kind === "yun");
    const flowChars = makeChars({ year: "  ", month: "  ", day: "  ", time: "  " }, flowEx).filter(c => c.kind === "flow");

    const keys = [];
    const transfers = [];
    const out = Object.assign({ 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 }, el || {});
    const counts = {};
    function bump(type) { counts[type] = (counts[type] || 0) + 1; }
    function push(k) {
      const base = IMASK[k.type] || 1.0;
      k.intensity = +(base * E(k.a != null ? k.a : 0)).toFixed(3);
      k.manifest = k.intensity >= 2.0;
      keys.push(k); bump(k.type);
    }
    function loss(wxName, unit, e) { if (out[wxName] != null) out[wxName] *= (1 + unit * e); }
    function gain(wxName, unit, e) { if (out[wxName] != null) out[wxName] *= (1 + unit * e); }
    const schoolPairs = sharedRootPairs(pillars);
    const allGan = natalChars.concat(yunChars, flowChars).filter(c => c.layer === "gan");
    const allZhi0 = natalChars.concat(yunChars, flowChars).filter(c => c.layer === "zhi");
    const ctx = {
      sk, push, loss, gain, out, o5: false, transfers: transfers,
      applyKe: function (atk, vic, e) { applyKe(out, atk, vic, e, sk); },
      dayStem: pillars.day ? pillars.day[0] : "",
      sharedPairs: schoolPairs,
      pillars: pillars,
      monthBr: pillars.month && pillars.month[1],
      heStems: collectGanHeStems(allGan),
      zhiSet: allZhi0.map(function (c) { return c.ch; }),
      openedVaults: {},
      zhiPass: "",
    };

    function runSamePillarGZ(list) {
      const byP = {};
      list.forEach(c => { (byP[c.pillar + ":" + (c.kind || "natal")] = byP[c.pillar + ":" + (c.kind || "natal")] || []).push(c); });
      Object.keys(byP).forEach(key => {
        const gan = byP[key].find(c => c.layer === "gan");
        const zhi = byP[key].find(c => c.layer === "zhi");
        if (gan && zhi) applyPair(gan, zhi, ctx);
      });
    }

    function runPairs(list, o5) {
      ctx.o5 = !!o5;
      const gan = list.filter(c => c.layer === "gan");
      const zhi = list.filter(c => c.layer === "zhi");
      ctx.zhiPass = "special";
      for (let i = 0; i < zhi.length; i++) for (let j = i + 1; j < zhi.length; j++) applyPair(zhi[i], zhi[j], ctx);
      ctx.zhiPass = "ordinary";
      for (let i = 0; i < zhi.length; i++) for (let j = i + 1; j < zhi.length; j++) applyPair(zhi[i], zhi[j], ctx);
      ctx.zhiPass = "";
      for (let i = 0; i < gan.length; i++) for (let j = i + 1; j < gan.length; j++) applyPair(gan[i], gan[j], ctx);
      runSamePillarGZ(list);
    }

    // ① 原局柱序
    runPairs(natalChars, false);
    // ② 自身运：大运→小运 对原局（柱距；小运×1/8）
    yunChars.forEach(yc => {
      natalChars.forEach(nc => { if (yc.layer === nc.layer) applyPair(nc, yc, ctx); });
    });
    if (yunChars.length >= 2) runPairs(yunChars, false);
    // ③ 流局内柱序（流年＝年…流时＝时）
    runPairs(flowChars, false);
    // ④ 流局 → 原局：分隔不生克，只特关／单字入局
    flowChars.forEach(fc => {
      natalChars.forEach(nc => { if (fc.layer === nc.layer) applyPair(nc, fc, Object.assign({}, ctx, { o5: true })); });
    });
    // ④′ 流局 → 运：流年 a=0 可生克；流月→时 a≥1 只特关
    flowChars.forEach(fc => {
      yunChars.forEach(yc => { if (fc.layer === yc.layer) applyPair(yc, fc, Object.assign({}, ctx, { o5: false })); });
    });

    // ⑤ 三字成键窗（全盘见字）+ 缺一退回两字（两字已在上列拆过）
    const allZhi = natalChars.concat(yunChars, flowChars).filter(c => c.layer === "zhi");
    const zhiSet = allZhi.map(c => c.ch);
    const has = ch => zhiSet.includes(ch);
    const windows = [];
    function tryTriple(groups, type, cls) {
      groups.forEach(g => {
        const n = [g[0], g[1], g[2]].filter(has).length;
        const w = tripleAB(g, type);
        const ok = n === 3 && inWindow(w.ab, type);
        windows.push({ type, cls, chars: g.slice(0, 3), hua: g[3], n, ab: w.ab, formed: ok, fallback: n < 3 || !ok });
        if (ok) {
          push({ type, cls, chars: g.slice(0, 3), hua: g[3], a: 0, note: `${g.slice(0, 3).join("")}${cls}${g[3]}（A+B=${w.ab} 成键；拆两字藏干四冲五合）` });
        }
      });
    }
    tryTriple(SANHUI, "sanhui", "三会方");
    tryTriple(SANHE, "sanhe", "三合局");
    XING_GROUPS.forEach(g => {
      if (has(g[0]) && has(g[1]) && has(g[2])) push({ type: "sanxing", cls: "三刑", chars: g.slice(), a: 0, note: `${g.join("")}三刑（三字齐·拆两字已计）` });
    });

    const ganList = natalChars.filter(c => c.layer === "gan");
    const tonggen = [];
    ganList.forEach(g => {
      PKEY.forEach((k, pi) => {
        const gz = pillars[k]; if (!gz) return;
        (HIDE[gz[1]] || []).forEach(([hs, w]) => {
          if (hs === g.ch) tonggen.push({ stem: g.ch, atPillar: PZH[k], weight: w, self: pi === g.pillar, note: `${g.ch}通根${PZH[PKEY[g.pillar]]}干→${PZH[k]}支(${gz[1]}藏${hs}·${w})` });
        });
      });
    });
    schoolPairs.forEach(p => {
      push({ type: "genqi", cls: relCls("genqi"), chars: p.stems.slice(), a: 0,
        layerZh: `同根${p.pillar}支${p.branch}`, note: p.note });
    });
    const vaultTrapped = vaultTrappedOf(pillars);
    const energyGanZhi = { 木: out.木, 火: out.火, 土: out.土, 金: out.金, 水: out.水 };
    applyTongGenAndChain(natalChars.concat(yunChars), pillars, {
      dayStem: ctx.dayStem, sharedPairs: schoolPairs,
    }, push);
    const shengChain = buildMuZiShengChain(pillars, ctx.dayStem);
    keys.forEach(k => { const n = counts[k.type] || 1; if (n > 1) k.intensity = +(k.intensity * (1 + 0.2 * (n - 1))).toFixed(3); k.manifest = k.intensity >= 2.0; });
    return {
      keys, tonggen, energyAfter: out, energyGanZhi, shengChain, counts,
      manifest: keys.filter(k => k.manifest),
      maxIntensity: keys.reduce((m, k) => Math.max(m, k.intensity), 0),
      windows, mode: "pipeline", transfers: transfers,
      sharedRoots: schoolPairs, vaultTrapped,
      orderZh: "年→月→日→时→大运→小运→流年→流月→流日→流时",
      o5Zh: "流层对原局分隔不生克，只经纠缠／单字共振入局；流年对运 a=0 可生克",
      ironZh: "a≥1只特关合冲 · 流原分隔不生克 · 流年对运a=0可生克 · 单字共振不流动 · 纠缠生克共振能烈都增",
    };
  }

  global.KRRelations = { scan, pipeline, E, relCls, tripleAB, inWindow,
    sharedRootPairs, vaultTrappedOf, stemRootsOf, natalTou,
    hehuaLingState, hehuaThreshold, hehuaChongPo, applyWuHeEnergy,
    collectGanHeStems, applySiChongEnergy, applyZhiSpecialHideEnergy,
    applyHideHeChongShengKe, twoCharHideOrder, isTightShengKe, isZhiSpecialPair, zhiPairSpecialTypes,
    formedTriplePair, hidesOf, buildMuZiShengChain, recordTonghang,
    yinBiOfBranch, zhiSpecialClass, isHeHuiPair, isXingChongPair,
    applyHeHuaMin, applyChongMin, applyShengMin, applyYinBiTransfer,
    natalDist, pairDist, openVault, applyHuaLiangAdd, applyTongziBoost,
    flowNatalSeparated, resonateKind,
    keMutualDelta, applyKe, skipShaKeViaRoot, genqiPairOf,
    tables: { LIUHE, LIUCHONG, LIUHAI, LIUPO, SANHE, SANHUI, BANHE, TIANGAN_HE, TIANGAN_CHONG, XING_GROUPS, VAULT } };
})(typeof window !== "undefined" ? window : globalThis);
