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', 'prd.md', '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', 'review/code-review.md', 'review/standards-review.md', 'release-notes.md', 'rollback.md', ]); const REVIEW_FILES = Object.freeze([ 'review/code-review.md', 'review/standards-review.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]; } /** * Review 报告不能只靠“文件存在”通过门禁。这里要求报告使用固定的最终结论行, * 避免 pending、blocked 或 changes_required 被一段其他说明文字意外判定为通过。 */ export function getUnpassedReviewFiles(reviewReports) { return REVIEW_FILES.filter((file) => { const content = reviewReports.get(file) ?? ''; return !/^最终结论:passed$/m.test(content); }); }