bookworm-smart-assistant/scripts/drift-guard.js

279 lines
8.4 KiB
JavaScript
Raw Normal View History

#!/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('系统无漂移 ✓');
}
}
}