bookworm-smart-assistant/scripts/patches/patch-l1c-rerank-arbitration-aware.js

179 lines
7.0 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Patch L1c: rerankTopK 仲裁感知化 (2026-04-25)
*
* 上下文 (承接 L1/L1b):
* L1b 完成后, disambiguation 层成功把 self-auditor (winner, ~0.881)
* 置于 project-audit-expert (loser, ~0.837) 之上. rerankTopK
* 随后给 loser _rerankBoost=1.2076 + _rerankProtected=true,
* loser.score 拉回 ~2.43 反超 winner. 端到端 self-auditor 失败.
*
* 根因:
* route-analyzer.js Line 597-616 :
* 1. r.disambiguated "曾被 boost 过"的单纯标记 (Line 860 设置),
* 不区分 winner / loser. L1b loser 仍带 disambiguated=true.
* 2. Line 599: `r.score *= max(rerankMultiplier, 1.0)` loser
* 也按 1.2076× 放大, 抹掉 L1b cap (winner.score * 0.95).
* 3. Line 600: `_rerankProtected=true` loser 加锁.
* 4. Line 608 cap: `topK.find(r => r.disambiguated)` 取第一个匹配,
* winner/loser disambiguated 时可能错把 loser cap 基线.
*
* 修复 (方案 A+B+C 组合, 选用最稳):
* A. loser ( _arbitratedBy 标记) 不进入 rerank 保护分支
* B. cap 基线改为"真 winner": disambiguated && !_arbitratedBy
* C. loser rerankMultiplier cap 1.0 (禁止再放大), 防越界
*
* 安全性:
* - 业务路由 (project-audit-expert "帮我审一下项目"场景独占 boost,
* _arbitratedBy 标记) 不受影响, 仍走 winner 保护分支.
* - boost 路径 (无跨域竞争) 行为完全不变 ( _arbitratedBy).
* - cold-start skill 发现 (_rerankBoost 主流程) 保留.
*
* Fail-close:
* 异常退化到旧行为 + stderr 警告. sentinel 防重复 patch.
*
* 用法:
* node patches/patch-l1c-rerank-arbitration-aware.js # dry-run
* node patches/patch-l1c-rerank-arbitration-aware.js --apply # 实际写入
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const TARGET = path.join(__dirname, '..', 'route-analyzer.js');
const SENTINEL = '/* L1c-RERANK-ARBITRATION-AWARE-2026-04-25 */';
const APPLY = process.argv.includes('--apply');
function fail(msg, code = 2) {
process.stderr.write('[L1c] ' + msg + '\n');
process.exit(code);
}
if (!fs.existsSync(TARGET)) fail('target missing: ' + TARGET);
const original = fs.readFileSync(TARGET, 'utf8');
// 检测行尾, patch 字符串用 LF, 必要时转 CRLF 匹配源文件
const EOL = original.includes('\r\n') ? '\r\n' : '\n';
const toEOL = (s) => EOL === '\r\n' ? s.replace(/\r?\n/g, '\r\n') : s;
if (original.includes(SENTINEL)) {
process.stdout.write('[L1c] sentinel present, already patched. no-op.\n');
process.exit(0);
}
// 精准锚点: 旧 rerank 保护代码块 (Line 595-605 区域)
const OLD_BLOCK =
` // rerank score = 原始 BM25 × (1 + jaccard×0.3 + tierRatio×0.2)
// 消歧 boosted 技能受保护: rerank 不降低其排名
const rerankMultiplier = 1 + jaccard * 0.3 + tierRatio * 0.2;
if (r.disambiguated) {
// 消歧已确认此技能优先: 只允许 rerank 增强,不允许被其他技能超越
r.score = r.score * Math.max(rerankMultiplier, 1.0);
r._rerankProtected = true;
} else {
r.score = r.score * rerankMultiplier;
}
r._rerankBoost = rerankMultiplier;`;
const NEW_BLOCK =
` // rerank score = 原始 BM25 × (1 + jaccard×0.3 + tierRatio×0.2)
// 消歧 boosted 技能受保护: rerank 不降低其排名
${SENTINEL}
const rerankMultiplier = 1 + jaccard * 0.3 + tierRatio * 0.2;
// L1c: L1b 仲裁 loser (_arbitratedBy 标记) 不享受保护, 且 multiplier 硬 cap 到 1.0
// 防止被 rerank boost 反超已被 cap 到 winner*0.95 的位置
const _isArbLoser = !!r._arbitratedBy;
if (r.disambiguated && !_isArbLoser) {
// 消歧 winner: 只允许 rerank 增强,不允许被其他技能超越
r.score = r.score * Math.max(rerankMultiplier, 1.0);
r._rerankProtected = true;
} else if (_isArbLoser) {
// 仲裁 loser: 严格不放大, 仅允许收紧 (jaccard/tier 真低分自然降级 OK)
const _capped = Math.min(rerankMultiplier, 1.0);
r.score = r.score * _capped;
r._rerankBoost = _capped;
continue;
} else {
r.score = r.score * rerankMultiplier;
}
r._rerankBoost = rerankMultiplier;`;
const OLD_CAP =
` // 消歧保护 cap: 非消歧技能不得超越消歧 top
const disambTop = topK.find(r => r.disambiguated);
if (disambTop) {
for (const r of topK) {
if (!r.disambiguated && r.score > disambTop.score) {
r.score = disambTop.score * 0.98;
r._rerankCapped = true;
}
}
}`;
const NEW_CAP =
` // 消歧保护 cap: 非消歧技能不得超越消歧 winner
// L1c: cap 基线必须是真 winner (disambiguated && !_arbitratedBy);
// 跨域仲裁 loser 虽然 disambiguated=true, 但被 L1b cap 到 winner*0.95,
// 不可作为 cap 基线 (否则真 winner 会被反向 cap)
const disambTop = topK.find(r => r.disambiguated && !r._arbitratedBy);
if (disambTop) {
for (const r of topK) {
if (r === disambTop) continue;
// 仲裁 loser 也参与 cap: 它的 disambiguated 是历史标记, 不豁免
if (r.score > disambTop.score) {
r.score = disambTop.score * 0.98;
r._rerankCapped = true;
}
}
}`;
const OLD_BLOCK_E = toEOL(OLD_BLOCK);
const NEW_BLOCK_E = toEOL(NEW_BLOCK);
const OLD_CAP_E = toEOL(OLD_CAP);
const NEW_CAP_E = toEOL(NEW_CAP);
if (!original.includes(OLD_BLOCK_E)) fail('anchor missing: rerank protected block');
if (!original.includes(OLD_CAP_E)) fail('anchor missing: disambig cap block');
const patched = original.replace(OLD_BLOCK_E, NEW_BLOCK_E).replace(OLD_CAP_E, NEW_CAP_E);
if (patched === original) fail('no-op replacement, abort to avoid silent failure');
if (!patched.includes(SENTINEL)) fail('sentinel missing post-patch');
// 语法预校验
try {
new (require('vm').Script)(patched, { filename: TARGET });
} catch (e) {
fail('syntax check failed: ' + e.message);
}
const oldHash = crypto.createHash('sha256').update(original).digest('hex').slice(0, 16);
const newHash = crypto.createHash('sha256').update(patched).digest('hex').slice(0, 16);
process.stdout.write('[L1c] target : ' + TARGET + '\n');
process.stdout.write('[L1c] old sha : ' + oldHash + '\n');
process.stdout.write('[L1c] new sha : ' + newHash + '\n');
process.stdout.write('[L1c] +' + (patched.split('\n').length - original.split('\n').length) + ' lines\n');
if (!APPLY) {
process.stdout.write('[L1c] dry-run OK. re-run with --apply to write.\n');
process.exit(0);
}
// 备份 + 原子写
const bakDir = path.join(__dirname, 'bak');
if (!fs.existsSync(bakDir)) fs.mkdirSync(bakDir, { recursive: true });
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const bakPath = path.join(bakDir, 'route-analyzer.js.' + ts + '.l1c.bak');
fs.writeFileSync(bakPath, original);
const tmp = TARGET + '.l1c.tmp';
fs.writeFileSync(tmp, patched);
fs.renameSync(tmp, TARGET);
process.stdout.write('[L1c] backup : ' + bakPath + '\n');
process.stdout.write('[L1c] applied OK.\n');