#!/usr/bin/env node /** * Patch C3: adaptive-disambiguator Dirichlet 根治 * - 懒初始化补 _initialAlphas 快照 (修 updateFromFeedback 路径) * - 竞争技能软衰减 (避免单调累积 → 权重塌陷) * - EMA 上限 (Σα ≤ 200,饱和后按比例回缩) * - adaptiveDisambiguate 归一化改 softmax-lite (不再用 maxAdj 线性缩放) * 幂等: sentinel C3_DIRICHLET_HARDENING_v1 */ const fs = require('fs'); const path = require('path'); const TARGET = path.join(__dirname, '..', 'adaptive-disambiguator.js'); const SENTINEL = 'C3_DIRICHLET_HARDENING_v1'; function main() { const src = fs.readFileSync(TARGET, 'utf8'); if (src.includes(SENTINEL)) { console.error('[patch-c3] already applied'); process.exit(0); } let patched = src; let hits = 0; // (1) 懒初始化补 _initialAlphas 快照 (L258-267) const old1 = " if (!state.pairs[key]) {\r\n" + " state.pairs[key] = {\r\n" + " alphas: {\r\n" + " [actualSkill]: CONFIG.weakPrior,\r\n" + " [competing]: CONFIG.weakPrior,\r\n" + " },\r\n" + " totalSamples: 0,\r\n" + " lastUpdated: null,\r\n" + " };\r\n" + " }"; // 兼容 LF 情况 const old1LF = old1.replace(/\r\n/g, '\n'); let use1 = null; if (patched.includes(old1)) use1 = old1; else if (patched.includes(old1LF)) use1 = old1LF; if (!use1) { console.error('[patch-c3] anchor1 miss'); process.exit(3); } const NL = use1.includes('\r\n') ? '\r\n' : '\n'; const new1 = " if (!state.pairs[key]) {" + NL + " // " + SENTINEL + ": 懒初始化必须保存 _initialAlphas 快照,否则 drift 报警失效" + NL + " const _alphas = {" + NL + " [actualSkill]: CONFIG.weakPrior," + NL + " [competing]: CONFIG.weakPrior," + NL + " };" + NL + " state.pairs[key] = {" + NL + " alphas: _alphas," + NL + " _initialAlphas: { ..._alphas }," + NL + " totalSamples: 0," + NL + " lastUpdated: null," + NL + " };" + NL + " }"; patched = patched.replace(use1, new1); hits++; // (2) updateFromFeedback α 更新: 加竞争软衰减 + EMA 上限 // 锚点: `pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;` const old2 = " pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;"; if (!patched.includes(old2)) { console.error('[patch-c3] anchor2 miss'); process.exit(4); } const new2 = " // " + SENTINEL + ": 正样本 +1; 竞争技能软衰减 (1%) 防止单调累积; EMA 上限 Σα ≤ 200" + NL + " pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;" + NL + " if (pair.alphas[competing] !== undefined && pair.alphas[competing] > CONFIG.weakPrior) {" + NL + " pair.alphas[competing] = Math.max(CONFIG.weakPrior," + NL + " pair.alphas[competing] - 0.01 * (pair.alphas[competing] - CONFIG.weakPrior));" + NL + " }" + NL + " // EMA 上限: 总样本量超过 200 时按比例回缩 (保持相对比例)" + NL + " const _sumAlpha = Object.values(pair.alphas).reduce(function (s, a) { return s + a; }, 0);" + NL + " if (_sumAlpha > 200) {" + NL + " const _scale = 200 / _sumAlpha;" + NL + " for (const _k of Object.keys(pair.alphas)) {" + NL + " pair.alphas[_k] = Math.max(CONFIG.weakPrior, pair.alphas[_k] * _scale);" + NL + " }" + NL + " }"; patched = patched.replace(old2, new2); hits++; // (3) adaptiveDisambiguate: softmax-lite 归一化 const old3 = " // 归一化 Bayesian 调整量到 [0, 1] 范围"; if (!patched.includes(old3)) { console.error('[patch-c3] anchor3 miss'); process.exit(5); } // 精确锚点: 替换从"归一化"注释到"let maxAdj = 0" 块后两行,但为安全只改注释标签,配合下方 result.map 内逻辑替换 // 更稳妥: 直接替换 map 里 `const normalizedAdj = maxAdj > 0 ? adj / maxAdj : 0;` const old3b = " const normalizedAdj = maxAdj > 0 ? adj / maxAdj : 0;"; if (!patched.includes(old3b)) { console.error('[patch-c3] anchor3b miss'); process.exit(6); } // 引入 softmax 归一化: 在函数内替换 maxAdj 计算块 + normalizedAdj 计算 const old3Block = " // 归一化 Bayesian 调整量到 [0, 1] 范围" + NL + " let maxAdj = 0;" + NL + " for (const adj of adjustments.values()) {" + NL + " if (Math.abs(adj) > maxAdj) maxAdj = Math.abs(adj);" + NL + " }"; const old3BlockLF = old3Block.replace(/\r\n/g, '\n'); let use3 = null; if (patched.includes(old3Block)) use3 = old3Block; else if (patched.includes(old3BlockLF)) use3 = old3BlockLF; if (!use3) { console.error('[patch-c3] anchor3 block miss'); process.exit(7); } const new3Block = " // " + SENTINEL + ": softmax-lite 归一化,避免 maxAdj 线性缩放导致的 ±1 饱和" + NL + " // Σ_i softmax(adj_i) = 1; 映射到 [-1, +1] 区间做 boost 基准" + NL + " const _expAdj = new Map();" + NL + " let _sumExp = 0;" + NL + " for (const [_k, _v] of adjustments.entries()) {" + NL + " const _e = Math.exp(Math.max(-5, Math.min(5, _v)));" + NL + " _expAdj.set(_k, _e);" + NL + " _sumExp += _e;" + NL + " }" + NL + " const _n = adjustments.size || 1;" + NL + " const _uniform = 1 / _n;" + NL + " // 兼容符号: 保留 maxAdj 供 fallback (若 softmax 退化则退回原算法)" + NL + " let maxAdj = 0;" + NL + " for (const adj of adjustments.values()) {" + NL + " if (Math.abs(adj) > maxAdj) maxAdj = Math.abs(adj);" + NL + " }"; patched = patched.replace(use3, new3Block); hits++; // 并替换 normalizedAdj 使用 softmax 结果 const new3b = " // " + SENTINEL + ": softmax 归一化 (退化时 fallback 到 maxAdj 线性)" + NL + " const _soft = _sumExp > 0 ? (_expAdj.get(c.name) || 0) / _sumExp : _uniform;" + NL + " // 映射 [0,1] → [-1,+1]: (soft - uniform) / uniform,再按 maxAdj 兜底" + NL + " const _softSigned = (_soft - _uniform) / Math.max(_uniform, 1e-6);" + NL + " const normalizedAdj = Math.max(-1, Math.min(1, _softSigned));"; patched = patched.replace(old3b, new3b); hits++; if (patched === src) { console.error('[patch-c3] no change'); process.exit(8); } if (hits < 4) { console.error('[patch-c3] expected 4 hits, got', hits); process.exit(9); } const bakDir = path.join(path.dirname(TARGET), 'patches', 'bak'); if (!fs.existsSync(bakDir)) fs.mkdirSync(bakDir, { recursive: true }); fs.writeFileSync(path.join(bakDir, 'adaptive-disambiguator.js.bak.c3.' + Date.now()), src); const tmp = TARGET + '.tmp.' + process.pid; fs.writeFileSync(tmp, patched); fs.renameSync(tmp, TARGET); console.error('[patch-c3] applied, hits=' + hits); } main();