264 lines
9.0 KiB
JavaScript
264 lines
9.0 KiB
JavaScript
/**
|
|
* 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<string, RegExp[]>}
|
|
*/
|
|
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;
|
|
}
|