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

242 lines
8.3 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
}