bookworm-smart-assistant/scripts/patches/patch-c1-atomic-reset.js

130 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* C1 补丁 2026-04-16 audit-2026-04-16 遗留 CRITICAL
*
* 目标: scripts/fusion-weight-learner.js 两处 unlinkSync 改为原子 tmp+rename
* - 位置 A: resetOnDegeneracy 分支 ( L331)
* - 位置 B: resetWeights() 函数 ( L354)
*
* 修复原理:
* 并发 readFileSync unlinkSync 存在 TOCTOU 竞态
* 改为写 DEFAULT_WEIGHTS tmp rename 覆盖
* 读者永远看到合法 JSON (或旧值不会 ENOENT)
*
* 幂等: 若已打过补丁 (存在 atomicResetWeightsFile 函数名) 则跳过
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', 'fusion-weight-learner.js');
const SENTINEL = 'atomicResetWeightsFile';
function main() {
if (!fs.existsSync(TARGET)) {
console.error('[patch-c1] 目标文件不存在:', TARGET);
process.exit(1);
}
const before = fs.readFileSync(TARGET, 'utf8');
if (before.includes(SENTINEL)) {
console.log('[patch-c1] 已打过补丁 (发现 ' + SENTINEL + '),跳过');
process.exit(0);
}
// ==== 注入 1: 在 _directWriteFallback 后追加 atomicResetWeightsFile 助手 ====
const helperAnchor =
'function _directWriteFallback(weightsFile, data) {\n' +
' try {\n' +
' if (!fs.existsSync(path.dirname(weightsFile))) {\n' +
' fs.mkdirSync(path.dirname(weightsFile), { recursive: true });\n' +
' }\n' +
' fs.writeFileSync(weightsFile, JSON.stringify(data, null, 2) + \'\\n\');\n' +
' } catch {}\n' +
'}';
const helperInjection =
helperAnchor +
'\n\n' +
'/**\n' +
' * C1 原子重置助手 (patch-c1-atomic-reset, 2026-04-16)\n' +
' * 用 tmp+rename 覆盖为 DEFAULT_WEIGHTS避免 unlinkSync 与并发 readFileSync 的 TOCTOU 竞态。\n' +
' * 读者永远看到合法 JSON (旧值或新的 DEFAULT),不会 ENOENT。\n' +
' */\n' +
'function atomicResetWeightsFile(weightsFile, reason) {\n' +
' try {\n' +
' if (!fs.existsSync(path.dirname(weightsFile))) {\n' +
' fs.mkdirSync(path.dirname(weightsFile), { recursive: true });\n' +
' }\n' +
' const payload = {\n' +
' weights: { ...DEFAULT_WEIGHTS },\n' +
' resetAt: new Date().toISOString(),\n' +
' resetReason: reason || \'manual\',\n' +
' };\n' +
' const _tmp = weightsFile + \'.tmp.\' + process.pid;\n' +
' fs.writeFileSync(_tmp, JSON.stringify(payload, null, 2) + \'\\n\');\n' +
' fs.renameSync(_tmp, weightsFile);\n' +
' } catch {}\n' +
'}';
if (!before.includes(helperAnchor)) {
console.error('[patch-c1] 找不到 _directWriteFallback 锚点,源文件已变更');
process.exit(2);
}
let after = before.replace(helperAnchor, helperInjection);
// ==== 注入 2: 替换 resetOnDegeneracy 分支的 unlinkSync (原 L331) ====
const degenPattern = /process\.stderr\.write\('\[fusion-weight\] 连续 ' \+ consecutiveHits \+ ' 周期退化命中reset 到 DEFAULT_WEIGHTS\\n'\);\s*\n\s*if \(fs\.existsSync\(FUSION_WEIGHTS_FILE\)\) fs\.unlinkSync\(FUSION_WEIGHTS_FILE\);/;
const degenReplacement =
"process.stderr.write('[fusion-weight] 连续 ' + consecutiveHits + ' 周期退化命中reset 到 DEFAULT_WEIGHTS\\n');\n" +
' atomicResetWeightsFile(FUSION_WEIGHTS_FILE, \'degen-consecutive-\' + consecutiveHits);';
if (!degenPattern.test(after)) {
console.error('[patch-c1] 找不到退化分支 unlinkSync 锚点');
process.exit(3);
}
after = after.replace(degenPattern, degenReplacement);
// ==== 注入 3: 替换 resetWeights() 的 unlinkSync (原 L354) ====
const resetPattern = /function resetWeights\(\) \{\s*\n\s*if \(fs\.existsSync\(FUSION_WEIGHTS_FILE\)\) \{\s*\n\s*fs\.unlinkSync\(FUSION_WEIGHTS_FILE\);\s*\n\s*\}\s*\n\s*return \{ status: 'reset', weights: DEFAULT_WEIGHTS \};\s*\n\}/;
const resetReplacement =
'function resetWeights() {\n' +
' atomicResetWeightsFile(FUSION_WEIGHTS_FILE, \'manual-reset\');\n' +
' return { status: \'reset\', weights: DEFAULT_WEIGHTS };\n' +
'}';
if (!resetPattern.test(after)) {
console.error('[patch-c1] 找不到 resetWeights() 锚点');
process.exit(4);
}
after = after.replace(resetPattern, resetReplacement);
// ==== 校验 ====
if (after === before) {
console.error('[patch-c1] 替换无效果,中止');
process.exit(5);
}
const unlinkCount = (after.match(/fs\.unlinkSync\(FUSION_WEIGHTS_FILE\)/g) || []).length;
if (unlinkCount !== 0) {
console.error('[patch-c1] 仍有 ' + unlinkCount + ' 处 unlinkSync(FUSION_WEIGHTS_FILE) 未清理');
process.exit(6);
}
// ==== 备份并原子写回 ====
const backup = TARGET + '.bak.c1.' + Date.now();
fs.writeFileSync(backup, before);
const _tmp = TARGET + '.tmp.' + process.pid;
fs.writeFileSync(_tmp, after);
fs.renameSync(_tmp, TARGET);
console.log('[patch-c1] ✓ 补丁应用成功');
console.log('[patch-c1] 备份: ' + path.basename(backup));
console.log('[patch-c1] 变更: 新增 atomicResetWeightsFile + 替换 2 处 unlinkSync');
}
try {
main();
} catch (e) {
console.error('[patch-c1] 异常:', e.message);
process.exit(99);
}