bookworm-smart-assistant/scripts/patches/patch-review-report-required.js

297 lines
11 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* review-report-required 补丁 2026-04-25 宪法 v1.4 2.1 P1-3 强制校验
*
* 背景:
* 10 天宪法真实作用评估发现: 交付自审报告执行率 < 30%, 条款成"软约束"
* 本补丁通过 post-edit-dispatcher 注入轻量提醒 + 合规日志, 做到:
* 1. 源码修改 >= 20 安全敏感路径 [review-required] 推送
* 2. 每次触发记录到 debug/review-compliance.log (含文件/行数/等级)
* 3. 不阻断 dispatcher 链路 (fail-open)
*
* 变更:
* (A) 新建 hooks/review-report-checker.js (独立模块, 导出 inlineCheck)
* (B) hooks/post-edit-dispatcher.js constitution-guard 调用之后注入一段
* require('./review-report-checker.js').inlineCheck(...) push messages
*
* 幂等:
* - sentinel: 'REVIEW_REPORT_REQUIRED_v1'
* - hooks/review-report-checker.js 已存在, 跳过创建
* - post-edit-dispatcher.js 已包含 sentinel, 跳过注入
*
* 回滚:
* - 删除 hooks/review-report-checker.js
* - 还原 hooks/post-edit-dispatcher.js.bak.review_report_required.*
*/
'use strict';
const fs = require('fs');
const path = require('path');
const CLAUDE_ROOT = path.join(__dirname, '..', '..');
const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks');
const CHECKER_PATH = path.join(HOOKS_DIR, 'review-report-checker.js');
const DISPATCHER_PATH = path.join(HOOKS_DIR, 'post-edit-dispatcher.js');
const SENTINEL = 'REVIEW_REPORT_REQUIRED_v1';
// ============================================================
// Step A: 新建 hooks/review-report-checker.js
// ============================================================
const CHECKER_SOURCE = [
'#!/usr/bin/env node',
"'use strict';",
'/**',
' * review-report-required - 宪法 v1.4 第 2.1 条强制校验模块 (P1-3, 2026-04-25)',
' *',
' * 背景: 10 天审计发现交付自审报告执行率 < 30%, 多被精简为单行 "审查: PASS"。',
' * 本模块在 post-edit-dispatcher 链路中以轻量方式推送 [review-required] 提示,',
' * 同时记录到 debug/review-compliance.log, 供 Stop hook / 周报汇总。',
' *',
' * 触发阈值:',
' * - 源代码扩展名 (.ts/.tsx/.js/.jsx/.py/.go/.rs/.java/.kt/.mjs/.cjs)',
' * - 行数 >= 20 或 文件路径命中安全敏感模式',
' *',
' * 输出等级:',
' * - SIMPLE: 单行 "审查: PASS/BLOCKED"',
' * - STANDARD: 4 维度 "=== AI CODE REVIEW REPORT ==="',
' * - + RED TEAM: 安全敏感模块 额外 5 问',
' * - + SEMANTIC DIFF: Edit 工具 + 修改 > 10 行',
' *',
' * 容错: 任何异常 fail-open 返回 null, 不阻断 dispatcher 链路。',
' * 补丁标记: ' + SENTINEL,
' */',
'',
"const path = require('path');",
"const fs = require('fs');",
'',
"const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.mjs', '.cjs'];",
'',
'// 安全敏感路径: hooks / src/auth / src/crypto / src/proxy / src/payment / constitution',
'const SECURITY_SENSITIVE_PATTERNS = [',
' /[\\\\/]hooks[\\\\/]/i,',
' /[\\\\/]src[\\\\/]auth/i,',
' /[\\\\/]src[\\\\/]crypto/i,',
' /[\\\\/]src[\\\\/]proxy/i,',
' /[\\\\/]src[\\\\/]payment/i,',
' /constitution/i,',
'];',
'',
'const MIN_LINES_FOR_CHECK = 20;',
'const STANDARD_TIER_THRESHOLD = 100;',
'const SEMANTIC_DIFF_THRESHOLD = 10;',
'',
'function extractContentAndLineCount(input) {',
' const ti = input && input.tool_input;',
" if (!ti) return { content: '', lineCount: 0 };",
" const content = ti.content || ti.new_string || '';",
' const lineCount = content ? content.split(String.fromCharCode(10)).length : 0;',
' return { content: content, lineCount: lineCount };',
'}',
'',
'function isSecuritySensitive(filePath) {',
' return SECURITY_SENSITIVE_PATTERNS.some(function (re) { return re.test(filePath); });',
'}',
'',
'function logCompliance(record) {',
' try {',
" const root = require('./lib/root.js');",
" const debugDir = path.join(root, 'debug');",
' if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });',
" const logPath = path.join(debugDir, 'review-compliance.log');",
" fs.appendFileSync(logPath, JSON.stringify(record) + '\\n');",
' } catch (_e) {}',
'}',
'',
'/**',
' * 核心入口: 由 post-edit-dispatcher 调用',
' * @param {string} filePath 被修改的文件路径',
' * @param {object} input 原始 PostToolUse input (含 tool_input / tool_name)',
' * @returns {string|null} systemMessage 片段, 无需提醒则返回 null',
' */',
'function inlineCheck(filePath, input) {',
' try {',
' if (!filePath) return null;',
' const ext = path.extname(filePath).toLowerCase();',
' if (!SOURCE_EXTENSIONS.includes(ext)) return null;',
'',
' const pair = extractContentAndLineCount(input);',
' const lineCount = pair.lineCount;',
' const sensitive = isSecuritySensitive(filePath);',
' const large = lineCount >= MIN_LINES_FOR_CHECK;',
' if (!sensitive && !large) return null;',
'',
" const tier = lineCount >= STANDARD_TIER_THRESHOLD ? 'STANDARD' : 'SIMPLE';",
' const toolName = input && input.tool_name;',
' const requireRedTeam = sensitive;',
" const requireSemanticDiff = lineCount > SEMANTIC_DIFF_THRESHOLD && toolName === 'Edit';",
'',
" const required = ['审查: PASS / BLOCKED (宪法 2.1)'];",
" if (tier === 'STANDARD') {",
" required.push('=== AI CODE REVIEW REPORT === (规范/安全/质量/架构 4 维度)');",
' }',
' if (requireRedTeam) {',
" required.push('=== RED TEAM SELF-REVIEW === (5 问对抗自审, 宪法 11.3)');",
' }',
' if (requireSemanticDiff) {',
" required.push('=== SEMANTIC DIFF === (逐行原始→修改→原因→副作用, 宪法 12.1)');",
' }',
'',
' logCompliance({',
' ts: new Date().toISOString(),',
' filePath: path.basename(filePath),',
' fullPath: filePath,',
' ext: ext,',
' lineCount: lineCount,',
' toolName: toolName || null,',
' isSensitive: sensitive,',
' tier: tier,',
' requiredCount: required.length,',
' });',
'',
" const header = '[review-required] ' + path.basename(filePath)",
" + ' 修改 ' + lineCount + ' 行'",
" + (sensitive ? ' (安全敏感)' : '')",
" + ' — 回复末尾必须附:';",
" const bullets = required.map(function (r) { return ' - ' + r; });",
' return [header].concat(bullets).join(String.fromCharCode(10));',
' } catch (_e) {',
' return null;',
' }',
'}',
'',
'module.exports = { inlineCheck: inlineCheck, isSecuritySensitive: isSecuritySensitive, SOURCE_EXTENSIONS: SOURCE_EXTENSIONS };',
'',
'// CLI 自测: node hooks/review-report-checker.js <filePath> <lineCount> [toolName]',
'if (require.main === module) {',
' const argv = process.argv.slice(2);',
' if (argv.length < 2) {',
" console.log('usage: node review-report-checker.js <filePath> <fakeLineCount> [toolName]');",
' process.exit(0);',
' }',
' const fakeInput = {',
" tool_name: argv[2] || 'Edit',",
" tool_input: { file_path: argv[0], new_string: 'x\\n'.repeat(parseInt(argv[1], 10) || 0) },",
' };',
' const out = inlineCheck(argv[0], fakeInput);',
" console.log(out === null ? '(no reminder)' : out);",
'}',
'',
].join('\n');
function writeAtomic(target, content) {
const tmp = target + '.tmp.' + process.pid;
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, target);
}
function stepACreateChecker() {
if (fs.existsSync(CHECKER_PATH)) {
const existing = fs.readFileSync(CHECKER_PATH, 'utf8');
if (existing.includes(SENTINEL)) {
console.log('[review-required] Step A 跳过: review-report-checker.js 已存在 (sentinel 命中)');
return;
}
const backup = CHECKER_PATH + '.bak.review_report_required.' + Date.now();
fs.writeFileSync(backup, existing);
console.log('[review-required] Step A 备份旧版: ' + path.basename(backup));
}
writeAtomic(CHECKER_PATH, CHECKER_SOURCE);
console.log('[review-required] ✓ Step A hooks/review-report-checker.js 写入完成');
}
// ============================================================
// Step B: 注入 post-edit-dispatcher.js
// ============================================================
function stepBInjectDispatcher() {
if (!fs.existsSync(DISPATCHER_PATH)) {
console.error('[review-required] Step B 失败: post-edit-dispatcher.js 不存在');
process.exit(1);
}
const before = fs.readFileSync(DISPATCHER_PATH, 'utf8');
if (before.includes(SENTINEL)) {
console.log('[review-required] Step B 跳过: post-edit-dispatcher 已打过补丁');
return;
}
// 锚点: constitution-guard 调用块结尾的 "} catch {}" + 一个空行 + 等待重型检查前的注释
// 用多候选锚点提高 CRLF/LF 兼容性
const anchorCandidates = [
' }\n\n // 等待重型检查完成 (并行)',
' }\r\n\r\n // 等待重型检查完成 (并行)',
];
const anchor = anchorCandidates.find(function (a) { return before.includes(a); });
if (!anchor) {
console.error('[review-required] Step B 失败: 锚点未匹配 (CRLF/LF 双候选都未命中)');
process.exit(2);
}
const eol = anchor.indexOf('\r\n') >= 0 ? '\r\n' : '\n';
const injection = [
' }',
'',
' // --- ' + SENTINEL + ': 宪法 2.1 审查报告强制提醒 (P1-3, 2026-04-25) ---',
' try {',
" const rr = require('./review-report-checker.js');",
' if (rr && rr.inlineCheck) {',
' const reviewMsg = rr.inlineCheck(filePath, input);',
' if (reviewMsg) messages.push(reviewMsg);',
' }',
' } catch (_e) {}',
'',
' // 等待重型检查完成 (并行)',
].join(eol);
const after = before.replace(anchor, injection);
if (after === before) {
console.error('[review-required] Step B 失败: replace 无效果');
process.exit(3);
}
const backup = DISPATCHER_PATH + '.bak.review_report_required.' + Date.now();
fs.writeFileSync(backup, before);
writeAtomic(DISPATCHER_PATH, after);
console.log('[review-required] ✓ Step B post-edit-dispatcher.js 注入完成');
console.log('[review-required] 备份: ' + path.basename(backup));
console.log('[review-required] 锚点 EOL: ' + (eol === '\r\n' ? 'CRLF' : 'LF'));
}
// ============================================================
// Step C: 语法烟测
// ============================================================
function stepCSmokeTest() {
const { execSync } = require('child_process');
try {
execSync('node -c "' + CHECKER_PATH + '"', { stdio: 'pipe' });
console.log('[review-required] ✓ Step C hooks/review-report-checker.js 语法 OK');
} catch (e) {
console.error('[review-required] Step C FAIL (checker):', e.message);
process.exit(4);
}
try {
execSync('node -c "' + DISPATCHER_PATH + '"', { stdio: 'pipe' });
console.log('[review-required] ✓ Step C hooks/post-edit-dispatcher.js 语法 OK');
} catch (e) {
console.error('[review-required] Step C FAIL (dispatcher):', e.message);
process.exit(5);
}
}
function main() {
console.log('[review-required] 补丁启动: sentinel = ' + SENTINEL);
stepACreateChecker();
stepBInjectDispatcher();
stepCSmokeTest();
console.log('[review-required] 全部完成 ✓ (3 步 / 幂等安全可重跑)');
}
try {
main();
} catch (e) {
console.error('[review-required] 异常:', e.message);
console.error(e.stack);
process.exit(99);
}