#!/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 [toolName]', 'if (require.main === module) {', ' const argv = process.argv.slice(2);', ' if (argv.length < 2) {', " console.log('usage: node review-report-checker.js [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); }