bookworm-smart-assistant/scripts/patches/patch-c3-dirichlet-hardening.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

144 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();