import path from 'node:path'; const FEATURE_ID_PATTERN = /^(?:FEAT-[A-Z]{2,8}|GOV)-\d{3}$/; const REQUIRED_FEATURE_FILES = Object.freeze([ 'manifest.yaml', 'requirements.md', 'design.md', 'implementation-plan.md', 'test-plan.md', 'gitnexus/before.md', 'gitnexus/impact-plan.md', 'gitnexus/detected-changes.md', 'gitnexus/after.md', 'gitnexus/comparison.md', 'qa/test-results.md', 'qa/visual-qa.md', 'release-notes.md', 'rollback.md', ]); /** * 功能编号是需求、设计、代码提交、测试结果和发布记录之间的主关联键。 * 严格限制格式可以避免路径穿越、临时名称以及后续无法检索的模糊编号。 */ export function validateFeatureId(featureId) { return FEATURE_ID_PATTERN.test(featureId); } /** * 功能目录只允许位于 docs/features 下。短名称继续使用英文 kebab-case, * 便于命令行、URL 和跨平台文件系统稳定处理;面向人的名称保存在 manifest 中。 */ export function getFeatureDirectory(repositoryRoot, featureId, slug) { if (!validateFeatureId(featureId)) { throw new Error(`功能编号格式无效:${featureId}`); } if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) { throw new Error(`功能短名称必须使用 kebab-case:${slug}`); } return path.join(repositoryRoot, 'docs', 'features', `${featureId}-${slug}`); } /** * 返回副本,防止调用方修改共享清单后绕过完成门禁。 */ export function getRequiredFeatureFiles() { return [...REQUIRED_FEATURE_FILES]; }