297 lines
11 KiB
JavaScript
297 lines
11 KiB
JavaScript
|
|
#!/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);
|
||
|
|
}
|