- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
139 lines
5.0 KiB
JavaScript
139 lines
5.0 KiB
JavaScript
/* 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);
|
|
}
|