/** * PR-37 Part D — Industry Feature Generator * * Generates domain-specific Feature objects for each epic, using the * domain registry's `coreFeatures` as a template. Falls back to generic * feature generation for unknown domains or epics without registered features. * * @module pr-37-domain-intelligence/industry-feature-generator * @since PR-37 */ import { getDomain } from "./domain-registry.mjs"; // ══════════════════════════════════════════════════════════ // Internal: ID counter // ══════════════════════════════════════════════════════════ let _featureCounter = 0; /** * Reset the internal feature ID counter (useful in tests). */ export function resetFeatureCounter() { _featureCounter = 0; } /** * Generate the next sequential feature ID. * * @returns {string} ID like "ft-001" */ function nextFeatureId() { _featureCounter++; return `ft-${String(_featureCounter).padStart(3, "0")}`; } // ══════════════════════════════════════════════════════════ // Feature type auto-detection // ══════════════════════════════════════════════════════════ /** * Feature type constants. * * @readonly * @enum {string} */ export const FEATURE_TYPE = Object.freeze({ CRUD: "CRUD", WORKFLOW: "workflow", ANALYTICS: "analytics", CONFIGURATION: "configuration", INTEGRATION: "integration", SEARCH: "search", NOTIFICATION: "notification", }); /** * Keywords that hint at each feature type. * * @type {object} */ const TYPE_PATTERNS = Object.freeze({ [FEATURE_TYPE.ANALYTICS]: [/分析/i, /报表/i, /看板/i, /统计/i, /趋势/i, /洞察/i, /仪表盘/i, /report/i, /dashboard/i, /analysis/i], [FEATURE_TYPE.CONFIGURATION]: [/配置/i, /参数/i, /设置/i, /规则/i, /策略/i, /模板/i, /setting/i, /config/i], [FEATURE_TYPE.INTEGRATION]: [/集成/i, /接口/i, /对接/i, /同步/i, /API/i, /导入/i, /导出/i, /import/i, /export/i, /webhook/i], [FEATURE_TYPE.SEARCH]: [/搜索/i, /检索/i, /查询/i, /查找/i, /search/i, /query/i], [FEATURE_TYPE.NOTIFICATION]: [/通知/i, /消息/i, /告警/i, /提醒/i, /预警/i, /notification/i, /alert/i, /remind/i], [FEATURE_TYPE.WORKFLOW]: [/流程/i, /审批/i, /审核/i, /流转/i, /工单/i, /workflow/i, /approval/i, /process/i], [FEATURE_TYPE.CRUD]: [/管理/i, /维护/i, /档案/i, /台账/i, /登记/i, /录入/i, /编辑/i, /创建/i], }); /** * Auto-detect the feature type based on the feature title/description. * * @param {string} title - Feature title * @param {string} [description=""] - Feature description * @returns {string} Detected feature type */ export function detectFeatureType(title, description = "") { const text = `${title} ${description}`; // Check patterns in priority order (CRUD last as fallback) const ordered = [ FEATURE_TYPE.ANALYTICS, FEATURE_TYPE.CONFIGURATION, FEATURE_TYPE.INTEGRATION, FEATURE_TYPE.SEARCH, FEATURE_TYPE.NOTIFICATION, FEATURE_TYPE.WORKFLOW, FEATURE_TYPE.CRUD, ]; for (const type of ordered) { const patterns = TYPE_PATTERNS[type]; for (const pattern of patterns) { if (pattern.test(text)) return type; } } // Default return FEATURE_TYPE.CRUD; } // ══════════════════════════════════════════════════════════ // Public API // ══════════════════════════════════════════════════════════ /** * Generate features for a set of epics within a given domain. * * @param {string} domainName - Target domain (e.g., "ERP", "MES", "CRM") * @param {object[]} epics - Array of epic objects (each must have at least `title`) * @returns {object[]} Array of feature objects, each with: * - id (string): "ft-xxx" * - title (string): Feature title * - description (string): Feature description * - epicId (string): ID of the parent epic * - featureType (string): Auto-detected feature type * * @example * const epics = generateIndustryEpics("WMS", "智能仓储"); * const features = generateIndustryFeatures("WMS", epics); * // => [ { id: "ft-001", title: "ASN收货", description: "...", epicId: "ep-001", featureType: "workflow" }, ... ] */ export function generateIndustryFeatures(domainName, epics) { if (!domainName || !epics || !Array.isArray(epics) || epics.length === 0) { return []; } const domain = getDomain(domainName); const domainFeatures = domain?.coreFeatures || {}; const allFeatures = []; for (const epic of epics) { const { id: epicId, title: epicTitle } = epic; // Look up domain-specific features for this epic title const featureNames = domainFeatures[epicTitle]; if (featureNames && Array.isArray(featureNames) && featureNames.length > 0) { // Domain-specific features for (const name of featureNames) { allFeatures.push({ id: nextFeatureId(), title: name, description: buildFeatureDescription(name, epicTitle, domain), epicId, featureType: detectFeatureType(name, epicTitle), }); } } else { // Fallback: generate generic features for this epic const generic = generateGenericFeaturesForEpic(epicTitle, epicId, domain); allFeatures.push(...generic); } } return allFeatures; } // ══════════════════════════════════════════════════════════ // Internal helpers // ══════════════════════════════════════════════════════════ /** * Build a meaningful description for a domain-specific feature. * * @param {string} featureName - Feature name * @param {string} epicTitle - Parent epic title * @param {object|null} domain - Domain definition (may be null) * @returns {string} */ function buildFeatureDescription(featureName, epicTitle, domain) { const domainLabel = domain ? `${domain.displayName} (${domain.name})` : "系统"; return `${domainLabel} — ${epicTitle} 模块下的「${featureName}」功能,支持相关业务操作与数据管理。`; } /** * Generate generic features for an epic when no domain-specific features exist. * * @param {string} epicTitle - Epic title * @param {string} epicId - Epic ID * @param {object|null} domain - Domain definition (may be null) * @returns {object[]} */ function generateGenericFeaturesForEpic(epicTitle, epicId, domain) { const domainLabel = domain ? `${domain.displayName} (${domain.name})` : "系统"; // Build generic CRUD + basic features const genericNames = detectGenericFeatures(epicTitle); return genericNames.map((name) => { const type = detectFeatureType(name, epicTitle); return { id: nextFeatureId(), title: name, description: `${domainLabel} — ${epicTitle} 模块提供「${name}」功能,支持相关业务数据的维护与管理。`, epicId, featureType: type, }; }); } /** * Detect sensible generic feature names for an epic based on its title. * * @param {string} epicTitle - Epic title * @returns {string[]} */ function detectGenericFeatures(epicTitle) { const titleLower = epicTitle.toLowerCase(); const featureMap = { // Match based on common patterns default: [ `${epicTitle}数据维护`, `${epicTitle}配置管理`, `${epicTitle}报表`, ], }; // Add analytics for most epics const hasAnalytics = /分析|报表|统计|看板|dashboard|report/i.test(titleLower); if (hasAnalytics) { return [ `${epicTitle}配置`, `${epicTitle}监控`, `${epicTitle}趋势分析`, `${epicTitle}导出`, ]; } return featureMap.default; } /** * Add domain-specific features for an epic by keyword matching. * This is a fallback enrichment when coreFeatures doesn't have a direct entry * but the epic title matches domain terminology. * * @param {string} title - Epic title * @param {object} domain - Domain definition * @returns {string[]|null} Matched feature names or null */ export function matchDomainFeaturesByKeyword(title, domain) { if (!domain || !domain.coreFeatures) return null; // Try partial title matching against registered epic titles const matchedEpicKey = Object.keys(domain.coreFeatures).find((key) => title.toLowerCase().includes(key.toLowerCase()) || key.toLowerCase().includes(title.toLowerCase()) ); if (matchedEpicKey) { return domain.coreFeatures[matchedEpicKey]; } return null; }