bookworm-smart-assistant/scripts/patches/patch-l1b-cross-boost-arbitration.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

148 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Patch L1b: Cross-Boost Arbitration (2026-04-25)
*
* 上下文:
* L1 修复让虚拟 agent 注入成功 (self-auditor base 0.702->1.032 +47%),
* 但实测 project-audit-expert 仍以 conf=1.0 抢占 primary.
*
* 真实根因 (与 brief 描述不同):
* "Phase 3 顺序依赖" 是误诊. Phase 3 仅在 boosted/penalty 列表存在交集
* 时强制排名, R27 与 R81/R84/R86 的 penalty 数组互不包含对方 boost 目标,
* Phase 3 根本未介入. 真实 bug 在 Phase 2:
* - R27 fires: project-audit-expert.score = base_pae * (1+boost_R27)
* - R81 fires: self-auditor.score = base_sa(virtual=_maxScore*0.6) * (1+boost_R81)
* pae 直接命中 系统/自检/审计 全部 BM25 高权重词, base_pae >> base_sa*0.6,
* 即使两者各自被 boost, pae 仍胜出. 无任何顺序敏感, 是分数基线不对等.
*
* 修复 (Phase 2.5 跨域仲裁):
* 1. 收集本轮所有 fired 规则中实际 boost 生效的 (skillName, ruleWeight*specificity) 元组
* 2. 检测跨域冲突: 多个不同 skillName 同被 boost, 且相互不在对方 penalty 列表
* 3. 按 (rule.weight * specificity) 仲裁: 最强规则的 boost 目标保持原分,
* 其他被 boost 目标按相对权重比例缩放: score *= max(0.6, weakWeight/strongWeight)
* 4. 同时把"输家" boost 目标的 score 上限钳制到不超过赢家 score
*
* 顺序无关性:
* 仲裁仅依赖 (boostVotes Map + firedRules + rule 静态属性), 不依赖遍历顺序.
* sort + Map.entries 保证确定性.
*
* Fail-close:
* 异常时跳过仲裁, stderr 警告, 不破坏现有 Phase 2/3.
*
* 使用:
* node patch-l1b-cross-boost-arbitration.js # dry-run
* node patch-l1b-cross-boost-arbitration.js --apply # 实际写入
*/
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawnSync } = require('child_process');
const TARGET = path.join(os.homedir(), '.claude', 'scripts', 'route-analyzer.js');
const SENTINEL = '// L1b-CROSS-BOOST-ARBITRATION (2026-04-25)';
const APPLY = process.argv.includes('--apply');
const ANCHOR = ' // Phase 3: 排名强制 — boosted 技能必须排在其 penalized 对手前面';
const INJECTION = ` // ${SENTINEL.replace('// ', '')}
// Phase 2.5: 跨域 boost 仲裁 — 防止两条 fired rule 各自 boost 不同 skill
// 但相互不在对方 penalty 列表中 (Phase 3 不介入), 导致基线分数高的胜出.
// 顺序无关: 仅依赖 boostVotes + 规则静态属性, 不依赖遍历次序.
try {
if (boostVotes.size >= 2) {
const _boostMeta = new Map();
for (const _rule of DISAMBIGUATION_RULES) {
if (!firedRules.includes(_rule.id)) continue;
if (!_rule.boost || !boostVotes.has(_rule.boost)) continue;
const _spec = computeRuleSpecificity(_rule.trigger.source);
const _w = (_rule.weight || 0) * (0.5 + _spec * 0.5);
const _prev = _boostMeta.get(_rule.boost);
if (!_prev || _w > _prev.weight) {
_boostMeta.set(_rule.boost, {
weight: _w,
ruleId: _rule.id,
penaltySet: new Set(_rule.penalty || [])
});
}
}
if (_boostMeta.size >= 2) {
const _ranked = Array.from(_boostMeta.entries())
.sort((a, b) => b[1].weight - a[1].weight);
const [_winnerName, _winnerMeta] = _ranked[0];
const _winner = results.find(r => r.name === _winnerName && r.score > 0);
if (_winner) {
for (let _i = 1; _i < _ranked.length; _i++) {
const [_loserName, _loserMeta] = _ranked[_i];
const _crossPenalty = _winnerMeta.penaltySet.has(_loserName)
|| _loserMeta.penaltySet.has(_winnerName);
if (_crossPenalty) continue;
const _loser = results.find(r => r.name === _loserName && r.score > 0);
if (!_loser) continue;
const _ratio = Math.max(0.6, _loserMeta.weight / Math.max(_winnerMeta.weight, 1e-6));
const _newScore = _loser.score * _ratio;
_loser.score = Math.min(_newScore, _winner.score * 0.95);
_loser._arbitratedBy = _winnerMeta.ruleId;
_loser._arbitrationRatio = Math.round(_ratio * 1000) / 1000;
}
}
}
}
} catch (_e) {
try { process.stderr.write('[route-analyzer] L1b cross-boost arbitration skipped: ' + (_e && _e.message ? _e.message : String(_e)) + '\\n'); } catch (_) {}
}
`;
function syntaxCheck(code) {
const tmp = path.join(os.tmpdir(), 'l1b-syncheck-' + process.pid + '.js');
fs.writeFileSync(tmp, code, 'utf8');
const r = spawnSync(process.execPath, ['--check', tmp], { encoding: 'utf8' });
try { fs.unlinkSync(tmp); } catch (_) {}
return { ok: r.status === 0, stderr: r.stderr || '' };
}
function main() {
if (!fs.existsSync(TARGET)) {
console.error('[L1b] target not found:', TARGET);
process.exit(2);
}
const src = fs.readFileSync(TARGET, 'utf8');
if (src.includes(SENTINEL)) {
console.log('[L1b] already applied (sentinel found). Skip.');
return;
}
const idx = src.indexOf(ANCHOR);
if (idx < 0) {
console.error('[L1b] anchor not found, cannot patch.');
process.exit(3);
}
const patched = src.slice(0, idx) + INJECTION + src.slice(idx);
const chk = syntaxCheck(patched);
if (!chk.ok) {
console.error('[L1b] generated code failed node --check:', chk.stderr);
process.exit(4);
}
if (!APPLY) {
console.log('[L1b] DRY-RUN ok: would inject', INJECTION.split('\n').length, 'lines at offset', idx);
console.log('[L1b] use --apply to write');
return;
}
const bak = TARGET + '.bak.l1b.' + Date.now();
fs.copyFileSync(TARGET, bak);
const tmp = TARGET + '.tmp.l1b.' + process.pid;
fs.writeFileSync(tmp, patched, 'utf8');
fs.renameSync(tmp, TARGET);
console.log('[L1b] APPLIED. backup:', path.basename(bak));
}
main();