bookworm-smart-assistant/scripts/patches/patch-c2-exit-code-normalize.js

149 lines
5.5 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* C2 补丁 2026-04-25 audit-2026-04-25 CRITICAL
*
* 目标: 规范两个 staging-pipeline 钩子的异常退出码 (exit(1) exit(2))
* - hooks/rollback-on-fail.js: L72 (bad-args), L97 (crash)
* - hooks/staging-validator.js: L80 (bad-args), L112 (crash)
*
* 修复原理:
* Claude Code hook 规范: exit(0)=通过, exit(2)=阻断并向 Claude 反馈 stderr,
* 其它非零码=非阻断错误bad-args crash 场景应显式返回 2
* 当前未注册所以无实际阻断但注册后会导致反馈异常
*
* 保留不动:
* - rollback-on-fail.js L93 `process.exit(restored ? 0 : 1)` 是成功/失败三态语义非阻断
* - staging-validator.js L108 `process.exit(passed ? 0 : 2)` 已正确
*
* 幂等: 通过 SENTINEL 字符串检测已打过则跳过
* 安全: .bak 备份 + tmp+rename 原子写 ( patch-c1 同模板)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const HOOKS_DIR = path.join(__dirname, '..', '..', 'hooks');
const SENTINEL = '/* patch-c2-exit-code-normalize:v1 */';
const TARGETS = [
{
file: path.join(HOOKS_DIR, 'rollback-on-fail.js'),
replacements: [
{
from: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-bad-args' });\n process.exit(1);",
to: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-bad-args' });\n process.exit(2);",
label: 'rollback-bad-args',
},
{
from: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-crash', error: String(e).slice(0, 200) });\n process.exit(1);",
to: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-crash', error: String(e).slice(0, 200) });\n process.exit(2);",
label: 'rollback-crash',
},
],
},
{
file: path.join(HOOKS_DIR, 'staging-validator.js'),
replacements: [
{
from: "appendManifest({ ts: new Date().toISOString(), event: 'validator-bad-args' });\n process.exit(1);",
to: "appendManifest({ ts: new Date().toISOString(), event: 'validator-bad-args' });\n process.exit(2);",
label: 'validator-bad-args',
},
{
from: "appendManifest({ ts: new Date().toISOString(), event: 'validator-crash', error: String(e).slice(0, 200) });\n process.exit(1);",
to: "appendManifest({ ts: new Date().toISOString(), event: 'validator-crash', error: String(e).slice(0, 200) });\n process.exit(2);",
label: 'validator-crash',
},
],
},
];
function atomicWrite(file, content) {
const tmp = file + '.tmp.' + process.pid + '.' + Date.now();
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, file);
}
function patchOne(target) {
const { file, replacements } = target;
const rel = path.relative(path.join(__dirname, '..', '..'), file);
if (!fs.existsSync(file)) {
console.error('[patch-c2] 目标文件不存在:', rel);
return { file: rel, status: 'missing', applied: 0 };
}
const before = fs.readFileSync(file, 'utf8');
if (before.includes(SENTINEL)) {
console.log('[patch-c2] ' + rel + ' 已打过补丁 (sentinel),跳过');
return { file: rel, status: 'skipped', applied: 0 };
}
let after = before;
const applied = [];
const missing = [];
for (const rep of replacements) {
if (after.includes(rep.from)) {
after = after.replace(rep.from, rep.to);
applied.push(rep.label);
} else if (after.includes(rep.to)) {
// 已是目标状态 (手动改过或半幂等场景)
applied.push(rep.label + ' [already-correct]');
} else {
missing.push(rep.label);
}
}
if (applied.length === 0) {
console.error('[patch-c2] ' + rel + ' 未找到任何锚点可能文件已大幅修改。missing=' + missing.join(','));
return { file: rel, status: 'no-anchor', applied: 0 };
}
// 注入 sentinel 到文件顶部注释块(紧跟 'use strict' 或文件开头)
const sentinelComment = SENTINEL + '\n';
if (!after.includes(SENTINEL)) {
if (after.startsWith("'use strict';")) {
after = after.replace("'use strict';", "'use strict';\n" + sentinelComment.trim());
} else {
after = sentinelComment + after;
}
}
if (after === before) {
console.log('[patch-c2] ' + rel + ' 无需改动');
return { file: rel, status: 'noop', applied: 0 };
}
// .bak 备份 (同目录) — hooks/ 受保护,退化到 scripts/patches/bak/
const bakDir = path.join(__dirname, 'bak');
if (!fs.existsSync(bakDir)) fs.mkdirSync(bakDir, { recursive: true });
const bakFile = path.join(bakDir, path.basename(file) + '.c2.' + Date.now() + '.bak');
fs.writeFileSync(bakFile, before);
atomicWrite(file, after);
console.log('[patch-c2] ' + rel + ' ✓ applied=[' + applied.join(', ') + '] bak=' + path.relative(path.join(__dirname, '..', '..'), bakFile));
if (missing.length > 0) {
console.warn('[patch-c2] ' + rel + ' ⚠ missing=' + missing.join(','));
}
return { file: rel, status: 'applied', applied: applied.length, missing };
}
function main() {
console.log('=== patch-c2-exit-code-normalize ===');
const results = TARGETS.map(patchOne);
const summary = {
applied: results.filter(r => r.status === 'applied').length,
skipped: results.filter(r => r.status === 'skipped').length,
failed: results.filter(r => r.status === 'missing' || r.status === 'no-anchor').length,
};
console.log('=== 完成 ===');
console.log(JSON.stringify(summary, null, 2));
process.exit(summary.failed > 0 ? 1 : 0);
}
main();