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

258 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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