bookworm-smart-assistant/scripts/domain-capacity-manager.js

417 lines
13 KiB
JavaScript
Raw Permalink 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)
*
* 当域内技能超过容量上限时,提供拆域建议。
* 为未来技能数量增长到 100+ 时的组织结构提前做好规划。
*
* 设计约束:
* - 每域建议上限 DOMAIN_CAPACITY = 12
* - fail-open: 所有分析失败不影响主路由流程
* - 只读分析,不修改任何配置文件
*
* 用法:
* node domain-capacity-manager.js # 打印分析报告
* node domain-capacity-manager.js --json # JSON 输出
* node domain-capacity-manager.js --suggest # 仅显示拆域建议
*/
'use strict';
const fs = require('fs');
const path = require('path');
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
const ROOT = detectClaudeRoot();
const INDEX_FILE = path.join(ROOT, 'skills-index.json');
// 每域建议上限
const DOMAIN_CAPACITY = 12;
// =====================================================
// 内部: 辅助函数
// =====================================================
/**
* 从技能名称和关键词中提取聚类标签
* 用于拆域建议时的子域聚类
*
* @param {string} skillName - 技能名称
* @param {Array} keywords - 技能关键词列表
* @returns {string[]} 标签列表
*/
function extractClusterTags(skillName, keywords) {
const tags = [];
const nameLower = skillName.toLowerCase();
// 基于名称推断子域标签
const tagPatterns = [
{ pattern: /frontend|react|vue|angular|next|css|tailwind|svelte/, tag: 'web-frontend' },
{ pattern: /backend|api|rest|graphql|node|fastapi|django|gin|express/, tag: 'backend-services' },
{ pattern: /mobile|flutter|swift|kotlin|react.native|expo|android|ios/, tag: 'mobile' },
{ pattern: /database|sql|redis|mongo|postgres|mysql|orm|query/, tag: 'data-storage' },
{ pattern: /devops|docker|k8s|kubernetes|ci.cd|pipeline|deploy/, tag: 'infrastructure' },
{ pattern: /security|auth|jwt|oauth|encrypt|pentest|owasp/, tag: 'security' },
{ pattern: /ai|ml|llm|rag|torch|tensorflow|embedding|model/, tag: 'ai-ml' },
{ pattern: /data|analyst|pandas|spark|etl|dbt|warehouse/, tag: 'data-engineering' },
{ pattern: /test|spec|jest|vitest|pytest|coverage|e2e/, tag: 'quality-testing' },
{ pattern: /architect|design|pattern|ddd|microservice|system/, tag: 'architecture' },
{ pattern: /automation|workflow|browser|playwright|selenium|scrape/, tag: 'automation' },
{ pattern: /cloud|aws|gcp|azure|vercel|edge|cdn|terraform/, tag: 'cloud' },
{ pattern: /typescript|python|golang|rust|java|swift/, tag: 'language-specific' },
{ pattern: /git|version|branch|merge|rebase|commit/, tag: 'vcs' },
{ pattern: /monitor|sre|slo|sli|alert|grafana|prometheus/, tag: 'observability' },
{ pattern: /product|prd|roadmap|sprint|agile|scrum/, tag: 'product-management' },
{ pattern: /design|ux|ui|figma|wcag|accessibility/, tag: 'design-ux' },
{ pattern: /content|write|doc|readme|seo|copy/, tag: 'content' },
{ pattern: /business|finance|sales|legal|marketing/, tag: 'business' },
{ pattern: /notification|push|sms|email|webhook/, tag: 'messaging' },
];
for (const { pattern, tag } of tagPatterns) {
if (pattern.test(nameLower)) {
tags.push(tag);
}
}
// 从关键词中补充标签(取前 10 个关键词)
const kwText = (keywords || []).slice(0, 10).map(k => k.keyword || '').join(' ').toLowerCase();
for (const { pattern, tag } of tagPatterns) {
if (!tags.includes(tag) && pattern.test(kwText)) {
tags.push(tag);
}
}
// 没有匹配则打 general 标签
if (tags.length === 0) tags.push('general');
return tags;
}
/**
* 基于技能的聚类标签,对超容量域内的技能进行简单分组
*
* @param {string[]} skillNames - 技能名称列表
* @param {Object} skillsMap - { skillName → skill对象 } 映射
* @returns {Object} { subDomain → skillNames[] }
*/
function clusterSkills(skillNames, skillsMap) {
const clusters = {}; // subDomain → skillNames[]
const assigned = new Set();
for (const name of skillNames) {
const skill = skillsMap[name];
if (!skill) continue;
const tags = extractClusterTags(name, skill.keywords || []);
// 取第一个标签作为主要子域
const primaryTag = tags[0] || 'general';
if (!clusters[primaryTag]) clusters[primaryTag] = [];
clusters[primaryTag].push(name);
assigned.add(name);
}
// 未分配的技能归入 general
for (const name of skillNames) {
if (!assigned.has(name)) {
if (!clusters['general']) clusters['general'] = [];
clusters['general'].push(name);
}
}
return clusters;
}
// =====================================================
// 公共 API
// =====================================================
/**
* 分析各域的技能容量
*
* @param {Object} [skillsIndex] - skills-index.json 内容(可选,不传则自动加载)
* @param {Object} [domainClassifier] - domain-classifier 模块(可选,不传则自动加载)
* @returns {Object} 分析结果
* {
* domains: {
* domainName: {
* skillCount: number,
* capacity: number,
* overCapacity: boolean,
* skills: string[],
* utilization: number // 0~1+
* }
* },
* overCapacityDomains: string[],
* totalSkills: number,
* totalDomains: number,
* suggestions: Object[] // 拆域建议列表
* }
*/
function analyze(skillsIndex, domainClassifier) {
try {
// 加载 skills-index
let index = skillsIndex;
if (!index) {
if (!fs.existsSync(INDEX_FILE)) {
return { error: 'skills-index.json 不存在', domains: {}, totalSkills: 0 };
}
index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
}
// 加载 domain-classifier
let classifier = domainClassifier;
if (!classifier) {
try {
classifier = require('./domain-classifier.js');
} catch {
return { error: 'domain-classifier.js 不可用', domains: {}, totalSkills: 0 };
}
}
const skills = index.skills || [];
const totalSkills = skills.length;
// 构建 skillName → skill 对象 的快速查找
const skillsMap = {};
for (const s of skills) {
skillsMap[s.name] = s;
}
// 使用 DOMAIN_SKILLS 映射统计各域技能数
const DOMAIN_SKILLS = classifier.DOMAIN_SKILLS || {};
const domains = {};
for (const [domain, domainSkills] of Object.entries(DOMAIN_SKILLS)) {
// 过滤只统计 index 中实际存在的技能
const existingSkills = domainSkills.filter(name => skillsMap[name]);
const skillCount = existingSkills.length;
domains[domain] = {
skillCount,
capacity: DOMAIN_CAPACITY,
overCapacity: skillCount > DOMAIN_CAPACITY,
skills: existingSkills,
utilization: Math.round(skillCount / DOMAIN_CAPACITY * 100) / 100,
};
}
// 检测未归类的技能(不在任何域 DOMAIN_SKILLS 中)
const allMappedSkills = new Set(
Object.values(DOMAIN_SKILLS).flat()
);
const unmapped = skills.filter(s => !allMappedSkills.has(s.name)).map(s => s.name);
if (unmapped.length > 0) {
domains['_unmapped'] = {
skillCount: unmapped.length,
capacity: DOMAIN_CAPACITY,
overCapacity: unmapped.length > DOMAIN_CAPACITY,
skills: unmapped,
utilization: Math.round(unmapped.length / DOMAIN_CAPACITY * 100) / 100,
};
}
// 标记超容量域
const overCapacityDomains = Object.keys(domains)
.filter(d => domains[d].overCapacity)
.sort((a, b) => domains[b].skillCount - domains[a].skillCount);
// 生成拆域建议
const suggestions = [];
for (const domain of overCapacityDomains) {
const suggestion = suggestSplit(domain, domains[domain].skills, skillsMap);
if (suggestion) suggestions.push(suggestion);
}
return {
domains,
overCapacityDomains,
totalSkills,
totalDomains: Object.keys(domains).length,
suggestions,
capacity: DOMAIN_CAPACITY,
};
} catch (err) {
// fail-open
return {
error: err.message,
domains: {},
overCapacityDomains: [],
totalSkills: 0,
totalDomains: 0,
suggestions: [],
};
}
}
/**
* 为超容量域生成拆域建议
*
* @param {string} domain - 域名称
* @param {string[]} skills - 域内技能名称列表
* @param {Object} [skillsMap] - { skillName → skill对象 } 映射(可选)
* @returns {Object|null} 拆域建议
* {
* domain: string,
* currentCount: number,
* proposedSubDomains: {
* name: string,
* skills: string[],
* rationale: string
* }[]
* }
*/
function suggestSplit(domain, skills, skillsMap) {
try {
if (!domain || !Array.isArray(skills) || skills.length <= DOMAIN_CAPACITY) {
return null;
}
// 如果没有提供 skillsMap尝试从文件加载
let sMap = skillsMap;
if (!sMap) {
try {
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
sMap = {};
for (const s of (index.skills || [])) sMap[s.name] = s;
} catch {
sMap = {};
}
}
// 聚类
const clusters = clusterSkills(skills, sMap);
// 合并过小的子域(< 2 个技能)到 general
const merged = {};
let generalSkills = clusters['general'] || [];
for (const [subDomain, subSkills] of Object.entries(clusters)) {
if (subDomain === 'general') continue;
if (subSkills.length < 2) {
generalSkills = generalSkills.concat(subSkills);
} else {
merged[subDomain] = subSkills;
}
}
if (generalSkills.length > 0) {
merged['general'] = generalSkills;
}
// 生成建议
const proposedSubDomains = [];
for (const [subName, subSkills] of Object.entries(merged)) {
if (subSkills.length === 0) continue;
// 生成子域命名建议
const fullName = `${domain}-${subName}`;
const rationale = subSkills.length > DOMAIN_CAPACITY
? `${subSkills.length} 个技能,仍超容量,建议进一步拆分`
: `${subSkills.length} 个技能,在容量范围内`;
proposedSubDomains.push({
name: fullName,
skills: subSkills,
skillCount: subSkills.length,
rationale,
});
}
// 按技能数量降序排列
proposedSubDomains.sort((a, b) => b.skillCount - a.skillCount);
return {
domain,
currentCount: skills.length,
capacityLimit: DOMAIN_CAPACITY,
overBy: skills.length - DOMAIN_CAPACITY,
proposedSubDomains,
totalProposedDomains: proposedSubDomains.length,
};
} catch {
return null;
}
}
/**
* 打印域容量分析报告(人类可读格式)
*
* @param {Object} [skillsIndex] - 可选,直接传入 index
* @param {Object} [domainClassifier] - 可选,直接传入 classifier
*/
function report(skillsIndex, domainClassifier) {
const result = analyze(skillsIndex, domainClassifier);
if (result.error) {
console.error(`[domain-capacity] 错误: ${result.error}`);
return result;
}
console.log(`\n=== 域容量分析报告 ===`);
console.log(`总技能数: ${result.totalSkills} | 域数: ${result.totalDomains} | 容量上限: ${result.capacity}/域\n`);
// 按技能数降序排列域
const sortedDomains = Object.entries(result.domains)
.sort(([, a], [, b]) => b.skillCount - a.skillCount);
for (const [domain, info] of sortedDomains) {
const bar = Math.round(info.utilization * 10);
const barStr = '█'.repeat(Math.min(bar, 12)) + '░'.repeat(Math.max(0, 12 - bar));
const overMark = info.overCapacity ? ' ← 超容量!' : '';
const pct = Math.round(info.utilization * 100);
console.log(` ${domain.padEnd(15)} ${barStr} ${String(info.skillCount).padStart(3)}/${result.capacity} (${pct}%)${overMark}`);
}
// 拆域建议
if (result.suggestions.length > 0) {
console.log(`\n--- 拆域建议 ---`);
for (const sug of result.suggestions) {
console.log(`\n[${sug.domain}] ${sug.currentCount} 个技能,超出 ${sug.overBy}`);
console.log(` 建议拆分为 ${sug.totalProposedDomains} 个子域:`);
for (const sub of sug.proposedSubDomains) {
console.log(`${sub.name} (${sub.skillCount} 技能): ${sub.skills.slice(0, 4).join(', ')}${sub.skills.length > 4 ? '...' : ''}`);
console.log(` ${sub.rationale}`);
}
}
} else {
console.log(`\n所有域均在容量范围内,无需拆分。`);
}
console.log();
return result;
}
// =====================================================
// 模块导出
// =====================================================
if (typeof module !== 'undefined') {
module.exports = {
analyze,
suggestSplit,
report,
DOMAIN_CAPACITY,
};
}
// CLI 入口
if (require.main === module) {
const args = process.argv.slice(2);
const jsonMode = args.includes('--json');
const suggestOnly = args.includes('--suggest');
if (jsonMode) {
const result = analyze();
console.log(JSON.stringify(result, null, 2));
} else if (suggestOnly) {
const result = analyze();
if (result.suggestions.length === 0) {
console.log('所有域均在容量范围内,无拆域建议。');
} else {
console.log(JSON.stringify(result.suggestions, null, 2));
}
} else {
report();
}
}