bookworm-smart-assistant/scripts/validate-loop-state.js

242 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* B6: Loop 状态文件完整性校验工具
*
* 校验 loop-state/ 目录下所有状态文件的完整性:
* - resume-prompt.md: 非空 + sentinel 标记 + HMAC + 注入检测
* - progress.md: 非空 + sentinel 标记
* - loop-state.json: HMAC 签名 + schema 校验
*
* 用法:
* node validate-loop-state.js # 校验全部
* node validate-loop-state.js --file resume # 校验单个
* node validate-loop-state.js --fix # 尝试自动修复
*
* 退出码: 0=全部通过, 1=有问题, 2=有问题但已修复
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const CLAUDE_HOME = process.env.CLAUDE_HOME || path.join(os.homedir(), '.claude');
const LOOP_DIR = path.join(CLAUDE_HOME, 'loop-state');
let stateIntegrity;
try {
stateIntegrity = require(path.join(CLAUDE_HOME, 'hooks', 'lib', 'state-integrity.js'));
} catch {
console.error('ERROR: 无法加载 state-integrity.js');
process.exit(1);
}
const args = process.argv.slice(2);
const fixMode = args.includes('--fix');
const targetFile = args.includes('--file') ? args[args.indexOf('--file') + 1] : null;
const results = [];
let hasIssue = false;
let hasFixed = false;
function check(name, status, detail) {
results.push({ name, status, detail });
if (status === 'FAIL') hasIssue = true;
if (status === 'FIXED') hasFixed = true;
const icon = status === 'PASS' ? '\x1b[32m✓\x1b[0m' : status === 'FIXED' ? '\x1b[33m⚡\x1b[0m' : '\x1b[31m✗\x1b[0m';
console.log(` ${icon} ${name}: ${detail}`);
}
// ─── resume-prompt.md 校验 ───────────────────────────────
function validateResumePrompt() {
const fp = path.join(LOOP_DIR, 'resume-prompt.md');
console.log('\n📄 resume-prompt.md');
// 存在性
if (!fs.existsSync(fp)) {
check('resume-exists', 'FAIL', '文件不存在');
return;
}
// 非空
const content = fs.readFileSync(fp, 'utf8');
if (!content.trim()) {
check('resume-nonempty', 'FAIL', '文件为空');
return;
}
check('resume-nonempty', 'PASS', content.length + ' 字符');
// Sentinel 标记
if (content.includes('<!-- COMPLETE -->')) {
check('resume-sentinel', 'PASS', '完整性标记存在');
} else {
check('resume-sentinel', 'FAIL', '缺少 <!-- COMPLETE --> 标记 (可能是上轮半写中断)');
if (fixMode) {
// 尝试使用备份
const bak = fp + '.bak';
if (fs.existsSync(bak) && fs.readFileSync(bak, 'utf8').includes('<!-- COMPLETE -->')) {
fs.copyFileSync(bak, fp);
check('resume-sentinel-fix', 'FIXED', '已从备份恢复');
}
}
}
// HMAC 签名
const result = stateIntegrity.readTextWithVerification(fp);
if (result.verified) {
check('resume-hmac', 'PASS', 'HMAC 签名验证通过');
} else if (result.error === 'sig-missing') {
check('resume-hmac', 'FAIL', '签名文件不存在');
if (fixMode) {
stateIntegrity.writeTextWithSignature(fp, content);
check('resume-hmac-fix', 'FIXED', '已重新生成签名');
}
} else {
check('resume-hmac', 'FAIL', '签名不匹配: ' + result.error);
}
// 注入检测
const sanitized = stateIntegrity.sanitizeResumePrompt(content);
if (sanitized.injectionDetected) {
check('resume-injection', 'FAIL', '检测到 ' + sanitized.warnings.length + ' 个疑似注入模式: ' + sanitized.warnings.join(', '));
if (fixMode) {
stateIntegrity.writeTextWithSignature(fp, sanitized.content);
check('resume-injection-fix', 'FIXED', '已清理注入标记并重新签名');
}
} else {
check('resume-injection', 'PASS', '无注入模式');
}
}
// ─── progress.md 校验 ────────────────────────────────────
function validateProgress() {
const fp = path.join(LOOP_DIR, 'progress.md');
console.log('\n📊 progress.md');
if (!fs.existsSync(fp)) {
check('progress-exists', 'FAIL', '文件不存在');
return;
}
const content = fs.readFileSync(fp, 'utf8');
if (!content.trim()) {
check('progress-nonempty', 'FAIL', '文件为空');
return;
}
check('progress-nonempty', 'PASS', content.length + ' 字符');
// 检查是否有步骤标记
const stepPattern = /(?:完成|completed|done|step|步骤)\s*\d/i;
if (stepPattern.test(content)) {
check('progress-steps', 'PASS', '包含步骤进度标记');
} else {
check('progress-steps', 'FAIL', '缺少步骤进度标记');
}
}
// ─── loop-state.json 校验 ────────────────────────────────
function validateLoopState() {
const fp = path.join(LOOP_DIR, 'loop-state.json');
console.log('\n⚙ loop-state.json');
if (!fs.existsSync(fp)) {
check('state-exists', 'FAIL', '文件不存在');
return;
}
// JSON 格式
let state;
try {
state = JSON.parse(fs.readFileSync(fp, 'utf8'));
check('state-json', 'PASS', 'JSON 格式正确');
} catch (e) {
check('state-json', 'FAIL', 'JSON 解析失败: ' + e.message);
return;
}
// Schema 校验
const requiredFields = ['phase', 'status', 'iteration', 'totalCostUsd', 'startTime'];
const missing = requiredFields.filter(f => !(f in state));
if (missing.length === 0) {
check('state-schema', 'PASS', '必填字段完整');
} else {
check('state-schema', 'FAIL', '缺少字段: ' + missing.join(', '));
}
// 状态合理性
const validPhases = ['coding', 'quality', 'deploy', 'verify', 'done'];
const validStatuses = ['running', 'quality_passed', 'deployed', 'completed', 'aborted',
'timeout', 'budget_exceeded', 'stuck', 'max_iterations_reached',
'deploy_failed', 'deploy_blocked_migration'];
if (state.phase && !validPhases.includes(state.phase)) {
check('state-phase', 'FAIL', '未知 phase: ' + state.phase);
}
if (state.status && !validStatuses.includes(state.status)) {
check('state-status', 'FAIL', '未知 status: ' + state.status);
}
// 迭代次数合理性
if (typeof state.iteration === 'number' && state.iteration >= 0) {
check('state-iteration', 'PASS', '迭代 ' + state.iteration);
} else {
check('state-iteration', 'FAIL', '迭代次数无效: ' + state.iteration);
}
}
// ─── 孤儿文件检测 ────────────────────────────────────────
function checkOrphans() {
console.log('\n🗂 孤儿文件');
if (!fs.existsSync(LOOP_DIR)) {
check('orphans', 'PASS', 'loop-state/ 目录不存在');
return;
}
const files = fs.readdirSync(LOOP_DIR);
const tmpFiles = files.filter(f => /\.tmp\.\d+$/.test(f));
if (tmpFiles.length > 0) {
check('orphan-tmp', 'FAIL', tmpFiles.length + ' 个孤儿 .tmp 文件');
if (fixMode) {
for (const f of tmpFiles) {
try { fs.unlinkSync(path.join(LOOP_DIR, f)); } catch {}
}
check('orphan-tmp-fix', 'FIXED', '已清理 ' + tmpFiles.length + ' 个文件');
}
} else {
check('orphan-tmp', 'PASS', '无孤儿文件');
}
}
// ─── 主流程 ──────────────────────────────────────────────
console.log('=== Loop State Integrity Validator ===');
console.log('目录: ' + LOOP_DIR);
console.log('模式: ' + (fixMode ? '校验 + 修复' : '仅校验'));
if (!targetFile || targetFile === 'resume') validateResumePrompt();
if (!targetFile || targetFile === 'progress') validateProgress();
if (!targetFile || targetFile === 'state') validateLoopState();
if (!targetFile) checkOrphans();
// 汇总
console.log('\n─────────────────────────────');
const passed = results.filter(r => r.status === 'PASS').length;
const failed = results.filter(r => r.status === 'FAIL').length;
const fixed = results.filter(r => r.status === 'FIXED').length;
const skipped = results.filter(r => r.status === 'SKIP').length;
console.log(`结果: ${passed} PASS / ${failed} FAIL / ${fixed} FIXED / ${skipped} SKIP`);
if (failed > 0 && !hasFixed) {
console.log('\x1b[31m状态: INTEGRITY_FAIL\x1b[0m');
process.exit(1);
} else if (hasFixed) {
console.log('\x1b[33m状态: INTEGRITY_FIXED\x1b[0m');
process.exit(2);
} else {
console.log('\x1b[32m状态: INTEGRITY_OK\x1b[0m');
process.exit(0);
}