bookworm-smart-assistant/hooks/review-report-checker.js

139 lines
5.0 KiB
JavaScript
Raw Permalink Normal View History

/* patch-review-sealed-frame:v2 */
#!/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 链路
* 补丁标记: REVIEW_REPORT_REQUIRED_v1
*/
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('封印框 (模板 M): 规范/安全/质量/架构 4 维度 + BOOKWORM · CODE REVIEW ╔╗ 框');
}
if (requireRedTeam) {
required.push('封印框 (模板 L): CODE REVIEW + 红队 5 问分节 (§11.3)');
}
if (requireSemanticDiff) {
required.push('封印框 (模板 XL): 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);
}