bookworm-smart-assistant/hooks/drift-detector.js

222 lines
8.0 KiB
JavaScript

#!/usr/bin/env node
/**
* PostToolUse Hook: 配置漂移检测器
* 匹配器: Edit|Write|NotebookEdit
* 触发: 修改了 .claude/ 目录下的基础设施文件时
* 退出码: 0=静默通过, 2=提醒(非阻断,continue=true)
*
* 自进化系统核心组件:
* - 检测 .claude/ 下配置文件的修改
* - 提醒运行 self-auditor 检查一致性
* - 不阻断工作流,仅提供信息
* - 避免在 self-auditor/self-healer 运行时重复触发
*/
const path = require('path');
const { execFileSync } = require('child_process');
const readStdin = require('./lib/read-stdin.js');
// .claude/ 目录下的基础设施文件模式
const INFRA_PATTERNS = [
/[/\\]\.claude[/\\]settings\.json$/,
/[/\\]\.claude[/\\]CLAUDE\.md$/,
/[/\\]\.claude[/\\]SKILL-REGISTRY\.md$/,
/[/\\]\.claude[/\\]agents[/\\].*\.md$/,
/[/\\]\.claude[/\\]hooks[/\\].*\.js$/,
/[/\\]\.claude[/\\]skills[/\\].*[/\\]SKILL\.md$/,
/[/\\]\.claude[/\\]constitution[/\\].*\.md$/,
];
// 记忆文件 (修改时也应检查一致性,但优先级低)
const MEMORY_PATTERNS = [
/[/\\]\.claude[/\\]projects[/\\].*[/\\]memory[/\\].*\.md$/,
];
// 项目级宪法文件 (修改时提醒同步全局副本)
const CONSTITUTION_PATTERNS = [
/[/\\]constitution[/\\]AI-CONSTITUTION\.md$/,
];
// 白名单: 这些文件修改不触发 (避免无限循环)
const WHITELIST_PATTERNS = [
/evolution-log\.(md|jsonl)$/, // 进化日志本身 (md 归档 + jsonl 活跃)
/\.claude[/\\]\.gitignore$/, // git 配置
];
function classifyChange(filePath) {
// 白名单优先
if (WHITELIST_PATTERNS.some(p => p.test(filePath))) {
return 'ignore';
}
// 基础设施文件
if (INFRA_PATTERNS.some(p => p.test(filePath))) {
return 'infra';
}
// 项目级宪法文件
if (CONSTITUTION_PATTERNS.some(p => p.test(filePath))) {
return 'constitution';
}
// 记忆文件
if (MEMORY_PATTERNS.some(p => p.test(filePath))) {
return 'memory';
}
return 'unrelated';
}
function getChangeType(filePath) {
if (/settings\.json$/.test(filePath)) return '全局设置';
if (/CLAUDE\.md$/.test(filePath)) return '路由配置';
if (/SKILL-REGISTRY\.md$/.test(filePath)) return '技能清单';
if (/agents[/\\]/.test(filePath)) return '智能体';
if (/hooks[/\\]/.test(filePath)) return '钩子';
if (/skills[/\\]/.test(filePath)) return '技能';
if (/memory[/\\]/.test(filePath)) return '记忆';
if (/constitution[/\\]/.test(filePath)) return '宪法';
return '配置文件';
}
// 第 3 层防线: 版本一致性快速校验 (CLAUDE.md vs stats-compiled.json vs SKILL-REGISTRY.md)
function checkVersionDrift() {
try {
const fs = require('fs');
const claudeRoot = path.resolve(__dirname, '..');
const versions = {};
// 从 CLAUDE.md 提取版本
try {
const claudeMd = fs.readFileSync(path.join(claudeRoot, 'CLAUDE.md'), 'utf8');
const m = claudeMd.match(/Smart Assistant[^v]*(v\d+\.\d+(?:\.\d+)?)/);
if (m) versions.claudeMd = m[1];
} catch { /* ignore */ }
// 从 stats-compiled.json 提取版本
try {
const stats = JSON.parse(fs.readFileSync(path.join(claudeRoot, 'stats-compiled.json'), 'utf8'));
if (stats.version) versions.stats = stats.version;
} catch { /* ignore */ }
// 从 SKILL-REGISTRY.md 提取版本
try {
const registry = fs.readFileSync(path.join(claudeRoot, 'SKILL-REGISTRY.md'), 'utf8');
const m = registry.match(/技能清单\s*(v\d+\.\d+(?:\.\d+)?)/);
if (m) versions.registry = m[1];
} catch { /* ignore */ }
const unique = [...new Set(Object.values(versions))];
if (unique.length > 1) {
const detail = Object.entries(versions).map(([k, v]) => `${k}=${v}`).join(', ');
return ` ⚠ 版本漂移: ${detail},建议运行 self-auditor。`;
}
return '';
} catch { return ''; }
}
function main() {
readStdin({ maxSize: 1024 * 1024 }).then(input => {
const toolName = input.tool_name;
if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'NotebookEdit') {
process.exit(0);
return;
}
const filePath = (input.tool_input && (input.tool_input.file_path || input.tool_input.filePath || input.tool_input.notebook_path)) || '';
if (!filePath) {
process.exit(0);
return;
}
const classification = classifyChange(filePath);
if (classification === 'ignore' || classification === 'unrelated') {
process.exit(0);
return;
}
const changeType = getChangeType(filePath);
const fileName = path.basename(filePath);
// 第 2 层防线: 基础设施文件变更时自动重新生成统计
let versionDrift = '';
if (classification === 'infra') {
try {
const statsScript = path.join(__dirname, '..', 'scripts', 'generate-stats.js');
execFileSync(process.execPath, [statsScript, '--quiet'], { timeout: 5000 });
} catch { /* 统计生成失败不阻断工作流 */ }
// 第 3 层防线: 版本一致性快速校验
versionDrift = checkVersionDrift();
}
let message;
if (classification === 'constitution') {
message = `[drift-detector] 项目宪法变更: \`${fileName}\` 已修改。` +
` 请同步更新全局副本 (~/.claude/constitution/) 并检查项目级 CLAUDE.md 和适配器 (.cursorrules/.windsurfrules) 的一致性。`;
} else if (classification === 'infra') {
message = `[drift-detector] 基础设施变更: ${changeType} \`${fileName}\` 已修改。` +
` stats-compiled.json 已自动更新。` + versionDrift;
} else {
// memory 类型,优先级较低
message = `[drift-detector] 记忆文件 \`${fileName}\` 已更新。` +
` 如果同时修改了多个配置文件,建议最后运行一次一致性检查。`;
}
process.stderr.write(JSON.stringify({
continue: true,
suppressOutput: false,
systemMessage: message,
}));
process.exit(2);
}).catch(() => process.exit(0));
}
// 模块导出 (供测试使用)
if (typeof module !== 'undefined') {
/**
* 内联检查函数 (供 post-edit-dispatcher 委托调用)
* 包含 generate-stats 自动触发 — 这是独立版本与之前内联版本的关键差异
*/
function inlineCheck(filePath) {
try {
if (WHITELIST_PATTERNS.some(p => p.test(filePath))) return null;
if (/evolution-log\.(md|jsonl)$/.test(filePath)) return null;
if (/\.claude[\\/]\.gitignore$/.test(filePath)) return null;
const classification = classifyChange(filePath);
if (classification === 'ignore' || classification === 'unrelated') return null;
const fileName = path.basename(filePath);
// 基础设施文件变更时自动重新生成统计 (P1 修复: 之前内联版本缺失此步骤)
if (classification === 'infra') {
try {
const { execFileSync } = require('child_process');
const statsScript = path.join(__dirname, '..', 'scripts', 'generate-stats.js');
execFileSync(process.execPath, [statsScript, '--quiet'], { timeout: 5000 });
} catch { /* 统计生成失败不阻断工作流 */ }
const vd = checkVersionDrift();
return `[drift-detector] 基础设施变更: \`${fileName}\` 已修改。stats-compiled.json 已自动更新。` + vd;
}
if (classification === 'constitution') {
return `[drift-detector] 项目宪法变更: \`${fileName}\` 已修改。请同步全局副本 (~/.claude/constitution/) 和适配器。`;
}
if (classification === 'memory') {
return `[drift-detector] 记忆文件 \`${fileName}\` 已更新。如果同时修改了多个配置文件,建议最后运行一次一致性检查。`;
}
return null;
} catch { return null; }
}
module.exports = { INFRA_PATTERNS, MEMORY_PATTERNS, CONSTITUTION_PATTERNS, WHITELIST_PATTERNS, classifyChange, getChangeType, inlineCheck };
}
if (require.main === module) {
main();
}