bookworm-smart-assistant/scripts/generate-stats.js

352 lines
12 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* 系统统计自动生成器
*
* 扫描 hooks/skills/agents/settings.json生成 stats-compiled.json
* 消除手动声明计数作为唯一真相源
*
* 用法:
* node scripts/generate-stats.js # 生成 stats-compiled.json
* node scripts/generate-stats.js --json # 输出 JSON stdout (供其他脚本消费)
* node scripts/generate-stats.js --quiet # 静默模式 ( hook 调用)
*
* 输出文件: {root}/stats-compiled.json
*/
const fs = require('fs');
const path = require('path');
const detectRoot = () => require('./paths.config.js').PATHS.root;
const ROOT = detectRoot();
// === 扫描钩子 ===
function scanHooks() {
const hooksDir = path.join(ROOT, 'hooks');
const allFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
// 读取 settings.json 中已注册的钩子脚本
let registeredScripts = new Set();
try {
const settings = JSON.parse(fs.readFileSync(path.join(ROOT, 'settings.json'), 'utf8'));
const hooks = settings.hooks || {};
for (const [, entries] of Object.entries(hooks)) {
for (const entry of entries) {
for (const hook of (entry.hooks || [])) {
if (hook.command) {
// 修复: 从命令中提取 .js 文件名 (处理重定向/|| true 后缀)
const jsMatch = hook.command.match(/[\w.\/\\-]+\.js/);
if (jsMatch) {
registeredScripts.add(path.basename(jsMatch[0]));
}
}
}
}
}
} catch { /* settings.json 不存在时跳过 */ }
// 分类: 按 hook 类型 (从注释头或文件名推断)
const PRE_PATTERNS = ['block-sensitive', 'block-dangerous', 'commit-message-lint', 'route-compliance'];
const ROUTE_PATTERNS = ['route-interceptor', 'subagent-route-injector', 'route-auditor'];
let preCount = 0, postCount = 0, routeCount = 0;
const registered = [];
const unregistered = [];
for (const file of allFiles) {
// 排除测试目录中的文件
if (file.startsWith('__')) continue;
const isRegistered = registeredScripts.has(file);
const info = { file, registered: isRegistered };
if (ROUTE_PATTERNS.some(p => file.includes(p))) {
info.type = 'route';
routeCount++;
} else if (PRE_PATTERNS.some(p => file.includes(p))) {
info.type = 'pre';
preCount++;
} else {
info.type = 'post';
postCount++;
}
if (isRegistered) {
registered.push(info);
} else {
unregistered.push(info);
}
}
return {
total: allFiles.length,
registered: registered.length,
unregistered: unregistered.length,
breakdown: { pre: preCount, post: postCount, route: routeCount },
unregisteredFiles: unregistered.map(u => u.file),
files: allFiles,
};
}
// === 扫描技能 ===
function scanSkills() {
const skillsDir = path.join(ROOT, 'skills');
if (!fs.existsSync(skillsDir)) return { total: 0, stable: 0, beta: 0, composable: 0, dirs: [] };
const dirs = fs.readdirSync(skillsDir).filter(d =>
fs.existsSync(path.join(skillsDir, d, 'SKILL.md'))
);
let stable = 0, beta = 0, composable = 0;
for (const dir of dirs) {
const content = fs.readFileSync(path.join(skillsDir, dir, 'SKILL.md'), 'utf8');
if (/maturity:\s*stable/.test(content)) stable++;
else if (/maturity:\s*beta/.test(content)) beta++;
if (/composable:\s*true/.test(content)) composable++; // COMPOSABLE_REGEX_FIX_v1
}
return { total: dirs.length, stable, beta, composable, dirs };
}
// === 扫描智能体 ===
function scanAgents() {
const agentsDir = path.join(ROOT, 'agents');
if (!fs.existsSync(agentsDir)) return { total: 0, opus: 0, sonnet: 0, files: [] };
const files = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
let opus = 0, sonnet = 0, haiku = 0;
for (const file of files) {
const content = fs.readFileSync(path.join(agentsDir, file), 'utf8');
if (/model:\s*opus/i.test(content)) opus++;
else if (/model:\s*sonnet/i.test(content)) sonnet++;
else if (/model:\s*haiku/i.test(content)) haiku++;
}
return { total: files.length, opus, sonnet, haiku, files };
}
// === 云托管 MCP 和插件 (非本地配置,手动维护) ===
const CLOUD_MCP = ['sentry', 'notion', 'gamma', 'canva', 'vercel', 'cloudinary', 'scholar-gateway', 'graphos'];
const PLUGIN_MCP = ['firebase'];
// === 扫描 MCP ===
function scanMCP() {
let localServers = [];
try {
// MCP 服务器配置在 ~/.claude.json (非 ~/.claude/settings.json)
const claudeJsonPath = path.join(path.dirname(ROOT), '.claude.json');
const config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
const servers = config.mcpServers || {};
localServers = Object.keys(servers);
} catch {
// 回退: 尝试 settings.json
try {
const settings = JSON.parse(fs.readFileSync(path.join(ROOT, 'settings.json'), 'utf8'));
const servers = settings.mcpServers || {};
localServers = Object.keys(servers);
} catch { /* 无配置 */ }
}
return {
local: localServers.length,
cloud: CLOUD_MCP.length,
plugin: PLUGIN_MCP.length,
total: localServers.length + CLOUD_MCP.length + PLUGIN_MCP.length,
servers: localServers,
cloudServers: CLOUD_MCP,
pluginServers: PLUGIN_MCP,
};
}
// === 扫描脚本 ===
function scanScripts() {
const scriptsDir = path.join(ROOT, 'scripts');
if (!fs.existsSync(scriptsDir)) return { total: 0, files: [] };
const files = fs.readdirSync(scriptsDir).filter(f => f.endsWith('.js'));
return { total: files.length, files };
}
// === 读取技能索引 ===
function readSkillsIndex() {
try {
const index = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills-index.json'), 'utf8'));
return {
skillCount: index.skillCount || 0,
totalKeywords: (index.skills || []).reduce((sum, s) => sum + (s.keywords || []).length, 0),
version: index.version || 'unknown',
};
} catch {
return { skillCount: 0, totalKeywords: 0, version: 'unknown' };
}
}
// === 主流程 ===
// === PHASE1_T1_3_MCP_OBSERVABILITY_FIELDS_2026_04_24 ===
// Phase 1 · T1.3: 读取 MCP 使用率 + 每日健康快照
function scanMcpObservability() {
const obs = { mcpUtilization: null, mcpHealth: null };
const usageFile = path.join(ROOT, 'mcp-usage-week.json');
const logsDir = path.join(ROOT, 'logs');
const today = new Date().toISOString().slice(0, 10);
const healthFile = path.join(logsDir, 'mcp-health-' + today + '.json');
try {
if (fs.existsSync(usageFile)) {
const u = JSON.parse(fs.readFileSync(usageFile, 'utf8'));
obs.mcpUtilization = {
schema_version: u.schema_version || 1,
generated: u.generated,
windowDays: u.windowDays,
totalEvents: u.totalEvents,
activeCount: Object.values(u.mcpStats || {}).filter(s => s.totalCalls > 0).length,
pruneCandidateCount: (u.pruneCandidates || []).length,
pruneCandidates: (u.pruneCandidates || []).map(p => p.server),
criticalCount: (u.criticalSet || []).length
};
}
} catch {}
try {
if (fs.existsSync(healthFile)) {
const h = JSON.parse(fs.readFileSync(healthFile, 'utf8'));
obs.mcpHealth = {
schema_version: h.schema_version || 1,
date: h.date,
probedAt: h.probedAt,
probeKind: h.probeKind,
totalMcps: h.totalMcps,
reachable: h.reachable,
unreachable: h.unreachable,
unreachableList: h.unreachableList
};
}
} catch {}
return obs;
}
// === END PHASE1_T1_3_MCP_OBSERVABILITY_FIELDS_2026_04_24 ===
function generateStats() {
const hooks = scanHooks();
const skills = scanSkills();
const agents = scanAgents();
const mcp = scanMCP();
const scripts = scanScripts();
const skillsIndex = readSkillsIndex();
const mcpObs = scanMcpObservability(); // T1.3
const stats = {
generated: new Date().toISOString(),
version: (function() {
try {
var claudeMd = require('fs').readFileSync(require('path').join(ROOT, 'CLAUDE.md'), 'utf8');
var m = claudeMd.match(/Smart Assistant[^v]*(v[\d.]+)/);
return m ? m[1] : 'v6.5';
} catch { return 'v6.5'; }
})(),
// 核心计数 (唯一真相源)
summary: {
skills: skills.total,
skillsStable: skills.stable,
skillsBeta: skills.beta,
skillsComposable: skills.composable,
agents: agents.total,
agentsOpus: agents.opus,
agentsSonnet: agents.sonnet,
agentsHaiku: agents.haiku,
hooks: hooks.total,
hooksRegistered: hooks.registered,
hooksUnregistered: hooks.unregistered,
hooksPre: hooks.breakdown.pre,
hooksPost: hooks.breakdown.post,
hooksRoute: hooks.breakdown.route,
mcp: mcp.local,
mcpCloud: mcp.cloud,
mcpPlugin: mcp.plugin,
mcpTotal: mcp.total,
scripts: scripts.total,
indexKeywords: skillsIndex.totalKeywords,
},
// 一致性校验
consistency: {
skillsMatch: skills.total === skillsIndex.skillCount,
skillsDir: skills.total,
skillsIndex: skillsIndex.skillCount,
},
// 未注册钩子清单 (供审计引用)
unregisteredHooks: hooks.unregisteredFiles,
// 设计决策: 审计时识别为有意选择,不扣分
designDecisions: {
unregisteredHooksIntentional: true,
reason: '备用钩子 (check-lint/check-typescript/integrity-check/suggest-tests) 设计为按需激活,不默认注册以避免每次文件操作额外延迟',
},
// Phase 1 · T1.3: MCP 观测字段 (PHASE1_T1_3_MCP_OBSERVABILITY_FIELDS_2026_04_24)
mcpUtilization: mcpObs.mcpUtilization,
mcpHealth: mcpObs.mcpHealth,
// 详细列表
details: {
hooks: hooks.files,
agents: agents.files,
mcp: mcp.servers,
mcpCloud: mcp.cloudServers,
mcpPlugin: mcp.pluginServers,
},
};
return stats;
}
function main() {
const args = process.argv.slice(2);
const jsonMode = args.includes('--json');
const quietMode = args.includes('--quiet');
const stats = generateStats();
// P2-V12: 原子写入 (temp+rename) 防止半写损坏
const outputPath = path.join(ROOT, 'stats-compiled.json');
const tmpPath = outputPath + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, JSON.stringify(stats, null, 2) + '\n');
fs.renameSync(tmpPath, outputPath);
if (jsonMode) {
process.stdout.write(JSON.stringify(stats));
return;
}
if (!quietMode) {
const s = stats.summary;
console.log('stats-compiled.json generated:');
console.log(` Skills: ${s.skills} (${s.skillsStable} stable, ${s.skillsBeta} beta, ${s.skillsComposable} composable)`);
console.log(` Agents: ${s.agents} (${s.agentsOpus} opus, ${s.agentsSonnet} sonnet, ${s.agentsHaiku} haiku)`);
console.log(` Hooks: ${s.hooks} (${s.hooksPre} pre + ${s.hooksPost} post + ${s.hooksRoute} route, ${s.hooksRegistered} registered, ${s.hooksUnregistered} unregistered)`);
console.log(` MCP: ${s.mcpTotal} (${s.mcp} local + ${s.mcpCloud} cloud + ${s.mcpPlugin} plugin)`);
console.log(` Scripts: ${s.scripts}`);
console.log(` Index: ${s.indexKeywords} keywords`);
if (!stats.consistency.skillsMatch) {
console.log(` [WARN] Skills mismatch: dir=${stats.consistency.skillsDir} vs index=${stats.consistency.skillsIndex}`);
}
console.log(` Output: ${outputPath}`);
}
}
// 导出供测试和其他脚本使用
if (typeof module !== 'undefined') {
module.exports = { generateStats, scanHooks, scanSkills, scanAgents, scanMCP, scanScripts };
}
if (require.main === module) {
main();
}