bookworm-smart-assistant/scripts/hook-priority-scheduler.js

258 lines
7.9 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* 钩子优先级调度器 (v6.0 Phase 3)
*
* 为钩子数量增长提供优先级调度能力
* 当会话工具调用次数增加时自动跳过低优先级钩子
* 减少高频场景下的调度开销
*
* 优先级定义:
* critical 始终执行安全相关绝不跳过
* high 正常执行路由合规编辑派遣宪法守卫
* medium 可跳过工具调用次数 > 50 时跳过
* low 高频场景跳过工具调用次数 > 20 时跳过
*
* 调度策略:
* toolCallCount <= 20 : 全部执行
* toolCallCount 21~50 : 跳过 low
* toolCallCount > 50 : 跳过 low + medium
* critical / high : 始终执行不受阈值限制
*
* 用法:
* const scheduler = require('./hook-priority-scheduler.js');
* scheduler.shouldExecute('activity-logger', 25); // → false
* scheduler.filterHooks(['post-edit-dispatcher', 'activity-logger'], 25); // → ['post-edit-dispatcher']
*/
// =====================================================
// 钩子优先级注册表
// =====================================================
/**
* 钩子名称 优先级映射
* 新增钩子时在此处注册未注册的钩子默认为 'high'
*
* @type {Object.<string, 'critical'|'high'|'medium'|'low'>}
*/
const HOOK_PRIORITIES = {
// === critical: 安全屏蔽,始终执行 ===
'block-sensitive-files': 'critical',
'block-dangerous-commands': 'critical',
// === high: 核心功能,正常执行 ===
'route-interceptor': 'high',
'route-compliance-gate': 'high',
'post-edit-dispatcher': 'high',
'constitution-guard': 'high',
'stop-hook': 'high',
'route-auditor': 'high',
// === medium: 质量辅助,高负载可跳过 ===
'post-edit-quality-check': 'medium',
'constitution-delivery-reminder': 'medium',
'suggest-tests': 'medium',
'memory-persistence-trigger': 'medium',
'check-typescript': 'medium',
'check-lint': 'medium',
'drift-detector': 'medium',
'integrity-check': 'medium',
'self-auditor': 'medium',
// === low: 日志记录,高频场景跳过 ===
'activity-logger': 'high', // V14 修复: 提升优先级,避免长会话断链
'build-outcome-tracker': 'low',
'route-stats-updater': 'low',
'evolution-log-writer': 'low',
};
// 工具调用次数阈值
const THRESHOLD_SKIP_LOW = 20; // 超过此值跳过 low 优先级
const THRESHOLD_SKIP_MEDIUM = 50; // 超过此值跳过 medium 优先级
// 优先级数字映射(数值越小优先级越高)
const PRIORITY_RANK = {
critical: 0,
high: 1,
medium: 2,
low: 3,
};
// =====================================================
// 公共 API
// =====================================================
/**
* 判断指定钩子在当前工具调用次数下是否应该执行
*
* @param {string} hookName - 钩子名称不含路径和扩展名
* @param {number} toolCallCount - 当前会话工具调用次数
* @returns {boolean} true 表示应该执行
*/
function shouldExecute(hookName, toolCallCount) {
try {
const priority = getPriority(hookName);
const count = (typeof toolCallCount === 'number' && toolCallCount >= 0)
? toolCallCount
: 0;
// critical 和 high 始终执行
if (priority === 'critical' || priority === 'high') return true;
// medium: 超过 50 次跳过
if (priority === 'medium' && count > THRESHOLD_SKIP_MEDIUM) return false;
// low: 超过 20 次跳过
if (priority === 'low' && count > THRESHOLD_SKIP_LOW) return false;
return true;
} catch {
// fail-open: 异常时默认执行
return true;
}
}
/**
* 获取钩子的优先级
*
* @param {string} hookName - 钩子名称
* @returns {'critical'|'high'|'medium'|'low'} 优先级字符串未注册返回 'high'
*/
function getPriority(hookName) {
if (!hookName || typeof hookName !== 'string') return 'high';
// 支持带路径和扩展名的钩子名(取文件名去扩展名)
const baseName = hookName
.replace(/\\/g, '/')
.split('/').pop()
.replace(/\.js$/, '');
return HOOK_PRIORITIES[baseName] || 'high';
}
/**
* 从钩子名列表中过滤出在当前工具调用次数下应执行的钩子
* post-edit-dispatcher.js 使用
*
* @param {string[]} hookNames - 钩子名称列表
* @param {number} toolCallCount - 当前会话工具调用次数
* @returns {string[]} 过滤后的钩子名称列表
*/
function filterHooks(hookNames, toolCallCount) {
if (!Array.isArray(hookNames)) return [];
return hookNames.filter(h => shouldExecute(h, toolCallCount));
}
/**
* 按优先级对钩子列表排序critical high medium low
*
* @param {string[]} hookNames - 钩子名称列表
* @returns {string[]} 排序后的列表
*/
function sortByPriority(hookNames) {
if (!Array.isArray(hookNames)) return [];
return [...hookNames].sort((a, b) => {
const rankA = PRIORITY_RANK[getPriority(a)] ?? 1;
const rankB = PRIORITY_RANK[getPriority(b)] ?? 1;
return rankA - rankB;
});
}
/**
* 读取当前工具调用计数
* debug/tool-call-counter.json 读取文件不存在或读取失败返回 0
*
* @param {string} [sessionId] - 会话 ID可选用于 per-session 计数
* @returns {number} 工具调用次数
*/
function readToolCallCount(sessionId) {
try {
const fs = require('fs');
const path = require('path');
const counterFile = path.join(
require('./paths.config.js').PATHS.root,
'debug', 'tool-call-counter.json'
);
if (!fs.existsSync(counterFile)) return 0;
const data = JSON.parse(fs.readFileSync(counterFile, 'utf8'));
// 如果提供了 sessionId优先读取 per-session 计数
if (sessionId && data.sessions && data.sessions[sessionId]) {
return data.sessions[sessionId].count || 0;
}
// 否则读取全局计数
return data.totalCount || data.count || 0;
} catch {
return 0;
}
}
/**
* 生成调度摘要报告供调试使用
*
* @param {number} toolCallCount - 工具调用次数
* @returns {Object} 包含各优先级执行情况的摘要
*/
function getScheduleSummary(toolCallCount) {
const count = toolCallCount || 0;
const result = {
toolCallCount: count,
thresholds: {
skipLow: THRESHOLD_SKIP_LOW,
skipMedium: THRESHOLD_SKIP_MEDIUM,
},
hooks: {},
};
for (const [name, priority] of Object.entries(HOOK_PRIORITIES)) {
result.hooks[name] = {
priority,
willExecute: shouldExecute(name, count),
};
}
return result;
}
// =====================================================
// 模块导出
// =====================================================
if (typeof module !== 'undefined') {
module.exports = {
// 核心调度接口
shouldExecute,
getPriority,
filterHooks,
sortByPriority,
// 工具函数
readToolCallCount,
getScheduleSummary,
// 常量导出(供测试和外部引用)
HOOK_PRIORITIES,
THRESHOLD_SKIP_LOW,
THRESHOLD_SKIP_MEDIUM,
};
}
// CLI 入口
if (require.main === module) {
const count = parseInt(process.argv[2]) || 0;
console.log(`=== 钩子优先级调度器 (toolCallCount=${count}) ===\n`);
const summary = getScheduleSummary(count);
const groups = { critical: [], high: [], medium: [], low: [] };
for (const [name, info] of Object.entries(summary.hooks)) {
const marker = info.willExecute ? '✓' : '✗';
groups[info.priority].push(` ${marker} ${name}`);
}
for (const [p, lines] of Object.entries(groups)) {
if (lines.length > 0) {
console.log(`[${p.toUpperCase()}]`);
console.log(lines.join('\n'));
console.log();
}
}
console.log(`阈值: low 跳过 > ${THRESHOLD_SKIP_LOW}, medium 跳过 > ${THRESHOLD_SKIP_MEDIUM}`);
}