#!/usr/bin/env node /** * 漂移防护守卫 (v5.9) * * 三层防漂移机制: * L1: 完整性校验 — checksums.json + HMAC (已有 integrity-check.js) * L2: 配置快照 — 关键配置文件的版本指纹 (本模块) * L3: 权重漂移检测 — 融合权重与关键词权重的偏移量监控 * * 用法: * node drift-guard.js --snapshot # 生成当前配置快照 * node drift-guard.js --check # 与上次快照对比 * node drift-guard.js --weights # 检测权重漂移 * node drift-guard.js --full # 全面检查 * node drift-guard.js --json # JSON 输出 * * 文件: * debug/config-snapshot.json 配置快照 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const detectClaudeRoot = () => require('./paths.config.js').PATHS.root; const ROOT = detectClaudeRoot(); const DEBUG_DIR = path.join(ROOT, 'debug'); const SNAPSHOT_FILE = path.join(DEBUG_DIR, 'config-snapshot.json'); // 需要监控的配置文件 const MONITORED_CONFIGS = [ 'CLAUDE.md', 'SKILL-REGISTRY.md', 'skills-index.json', 'hooks/checksums.json', 'scripts/disambiguation-rules.json', 'scripts/synonyms.json', ]; // 需要监控的运行时状态文件 const MONITORED_STATE = [ 'debug/fusion-weights.json', 'debug/route-weights.json', ]; function sha256(filePath) { try { return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); } catch { return null; } } /** * 生成当前配置快照 */ function generateSnapshot() { const snapshot = { generated: new Date().toISOString(), version: '5.9', configs: {}, state: {}, counts: {}, }; // 配置文件哈希 for (const file of MONITORED_CONFIGS) { const fp = path.join(ROOT, file); snapshot.configs[file] = sha256(fp); } // 状态文件哈希 for (const file of MONITORED_STATE) { const fp = path.join(ROOT, file); snapshot.state[file] = sha256(fp); } // 关键计数 try { const index = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills-index.json'), 'utf8')); snapshot.counts.skills = (index.skills || []).length; } catch { snapshot.counts.skills = 0; } try { const rules = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'disambiguation-rules.json'), 'utf8')); snapshot.counts.disambiguationRules = (rules.rules || []).length; } catch { snapshot.counts.disambiguationRules = 0; } try { const synonyms = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'synonyms.json'), 'utf8')); snapshot.counts.synonymGroups = (synonyms.groups || []).length; } catch { snapshot.counts.synonymGroups = 0; } snapshot.counts.hooks = fs.readdirSync(path.join(ROOT, 'hooks')).filter(f => f.endsWith('.js')).length; snapshot.counts.scripts = fs.readdirSync(path.join(ROOT, 'scripts')).filter(f => f.endsWith('.js')).length; return snapshot; } /** * 保存快照 */ function saveSnapshot(snapshot) { if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true }); fs.writeFileSync(SNAPSHOT_FILE, JSON.stringify(snapshot, null, 2) + '\n'); } /** * 加载上一次快照 */ function loadSnapshot() { try { if (fs.existsSync(SNAPSHOT_FILE)) { return JSON.parse(fs.readFileSync(SNAPSHOT_FILE, 'utf8')); } } catch {} return null; } /** * 对比当前状态与快照 * @returns {{ drifts: Array, summary: Object }} */ function checkDrift() { const prev = loadSnapshot(); if (!prev) return { drifts: [], summary: { status: 'no-baseline', message: '无快照基线,运行 --snapshot 生成' } }; const current = generateSnapshot(); const drifts = []; // 配置文件变更检测 for (const file of MONITORED_CONFIGS) { if (prev.configs[file] && current.configs[file] && prev.configs[file] !== current.configs[file]) { drifts.push({ type: 'config', file, severity: 'high', detail: '哈希不匹配' }); } if (prev.configs[file] && !current.configs[file]) { drifts.push({ type: 'config', file, severity: 'critical', detail: '文件缺失' }); } } // 计数异常检测 (±10% 阈值) for (const [key, prevCount] of Object.entries(prev.counts || {})) { const currCount = current.counts[key]; if (currCount === undefined) continue; const threshold = Math.max(2, Math.round(prevCount * 0.1)); if (Math.abs(currCount - prevCount) > threshold) { drifts.push({ type: 'count', key, severity: 'medium', detail: `${prevCount} → ${currCount} (变化 ${currCount - prevCount})`, }); } } const age = Date.now() - new Date(prev.generated).getTime(); const ageHours = Math.round(age / 3600000); return { drifts, summary: { status: drifts.length === 0 ? 'clean' : 'drift-detected', driftCount: drifts.length, criticals: drifts.filter(d => d.severity === 'critical').length, highs: drifts.filter(d => d.severity === 'high').length, snapshotAge: `${ageHours}h`, message: drifts.length === 0 ? `无漂移 (快照 ${ageHours}h 前)` : `检测到 ${drifts.length} 处漂移`, }, }; } /** * 检测权重漂移 */ function checkWeightDrift() { const findings = []; // 融合权重偏移 try { const fwl = require('./fusion-weight-learner.js'); const learned = fwl.loadWeights(); const defaults = fwl.DEFAULT_WEIGHTS; for (const [key, defVal] of Object.entries(defaults)) { const delta = Math.abs(learned[key] - defVal); if (delta > 0.15) { findings.push({ type: 'fusion-weight', key, severity: 'warning', detail: `${defVal} → ${learned[key]} (偏移 ${delta.toFixed(3)})`, }); } } } catch {} // 关键词权重偏移 try { const weightsFile = path.join(DEBUG_DIR, 'route-weights.json'); if (fs.existsSync(weightsFile)) { const weights = JSON.parse(fs.readFileSync(weightsFile, 'utf8')); const deltas = weights.deltas || {}; let maxDelta = 0, maxSkill = '', maxKw = ''; for (const [skill, kws] of Object.entries(deltas)) { for (const [kw, d] of Object.entries(kws)) { if (Math.abs(d) > maxDelta) { maxDelta = Math.abs(d); maxSkill = skill; maxKw = kw; } } } if (maxDelta > 0.3) { findings.push({ type: 'keyword-weight', severity: 'warning', detail: `最大偏移: ${maxSkill}/${maxKw} = ${maxDelta.toFixed(2)}`, }); } } } catch {} return findings; } // 模块导出 if (typeof module !== 'undefined') { module.exports = { generateSnapshot, saveSnapshot, loadSnapshot, checkDrift, checkWeightDrift, }; } // CLI 入口 if (require.main === module) { const args = process.argv.slice(2); const jsonMode = args.includes('--json'); if (args.includes('--snapshot')) { const snap = generateSnapshot(); saveSnapshot(snap); if (jsonMode) { console.log(JSON.stringify(snap, null, 2)); } else { console.log('配置快照已生成:'); console.log(` 配置文件: ${Object.keys(snap.configs).length}`); console.log(` 状态文件: ${Object.keys(snap.state).length}`); console.log(` 计数: skills=${snap.counts.skills}, hooks=${snap.counts.hooks}, scripts=${snap.counts.scripts}`); } process.exit(0); } if (args.includes('--weights')) { const findings = checkWeightDrift(); if (jsonMode) { console.log(JSON.stringify(findings, null, 2)); } else { console.log(`权重漂移检测: ${findings.length} 个告警`); for (const f of findings) console.log(` [${f.severity}] ${f.type}: ${f.detail}`); } process.exit(findings.length > 0 ? 1 : 0); } // --check or --full const drift = checkDrift(); const weightFindings = args.includes('--full') ? checkWeightDrift() : []; if (jsonMode) { console.log(JSON.stringify({ ...drift, weightDrift: weightFindings }, null, 2)); } else { console.log('=== 漂移防护检查 ==='); console.log(`状态: ${drift.summary.status}`); console.log(`快照年龄: ${drift.summary.snapshotAge || 'N/A'}`); if (drift.drifts.length > 0) { console.log(`\n漂移项:`); for (const d of drift.drifts) { console.log(` [${d.severity}] ${d.type}: ${d.file || d.key} — ${d.detail}`); } } if (weightFindings.length > 0) { console.log(`\n权重漂移:`); for (const f of weightFindings) console.log(` [${f.severity}] ${f.detail}`); } if (drift.drifts.length === 0 && weightFindings.length === 0) { console.log('系统无漂移 ✓'); } } }