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

417 lines
13 KiB
JavaScript
Raw Permalink Normal View History

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