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