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', 'development-summary.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', ]); const REQUIRED_EVIDENCE_SECTIONS = Object.freeze({ 'development-summary.md': [ '原始开发需求', '需求与范围变化', '实际完成需求', '修复缺陷', '未完成与延期项', '关键提交', '验证证据', ], 'design.md': ['安全策略与评估', '性能目标与评估'], 'test-plan.md': ['安全验证', '性能验证'], 'gitnexus/impact-plan.md': ['直接与间接影响', '跨模块同步修改'], 'review/code-review.md': ['安全与残余风险', '性能与资源影响', 'GitNexus 与同步修改'], 'review/standards-review.md': [ '安全、性能与架构检查', 'GitNexus 与开发总结检查', ], }); const EMPTY_EVIDENCE_PATTERN = /^(?:无|不适用|n\/?a|none)[。.!!]?$/i; /** * 功能编号是需求、设计、代码提交、测试结果和发布记录之间的主关联键。 * 严格限制格式可以避免路径穿越、临时名称以及后续无法检索的模糊编号。 */ 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]; } /** * 二级标题允许带文档章节序号,但证据名称必须完整匹配,避免靠散文中偶然出现的 * “安全”或“性能”字样绕过门禁。正文只剔除空行和 HTML 注释,不推断业务结论真伪。 */ function collectSecondLevelSections(content) { const sections = new Map(); let currentSection; for (const line of String(content).replace(/\r\n?/g, '\n').split('\n')) { const heading = line.match(/^##\s+(?:(?:\d+(?:\.\d+)*)\.\s+)?(.+?)\s*$/); if (heading) { currentSection = heading[1]; if (!sections.has(currentSection)) sections.set(currentSection, []); continue; } if (currentSection) sections.get(currentSection).push(line); } return sections; } function hasSubstantiveEvidence(lines) { const content = (lines ?? []) .join('\n') .replace(//g, '') .trim(); return content.length > 0 && !EMPTY_EVIDENCE_PATTERN.test(content); } /** * 返回全部无效证据而不是遇到首项即停止,使开发者一次即可补齐同一功能档案。 * “不适用”必须附带原因和适用边界;有解释的完整句子不会被当作空占位。 */ export function getInvalidFeatureEvidence(featureReports) { const invalidEvidence = []; for (const [file, requiredSections] of Object.entries(REQUIRED_EVIDENCE_SECTIONS)) { const sections = collectSecondLevelSections(featureReports.get(file) ?? ''); for (const section of requiredSections) { if (!hasSubstantiveEvidence(sections.get(section))) { invalidEvidence.push(`${file}:${section}`); } } } return invalidEvidence; } /** 后续功能的影响计划从完整检查维度起步,减少只分析主模块而遗漏消费者。 */ export function buildImpactPlanTemplate(featureId) { if (!validateFeatureId(featureId)) { throw new Error(`功能编号格式无效:${featureId}`); } return `# GitNexus 开发前影响计划 - 功能编号:${featureId} - 总体风险:待分析 ## 直接与间接影响 请记录 query、context、upstream impact、消费者和执行流。 ## 跨模块同步修改 请逐项判断 HTTP/OpenAPI、Web API、消息契约、数据库、Redis、API、Scheduler、Worker、配置部署、测试和文档。 `; } /** * Review 报告不能只靠“文件存在”通过门禁。这里要求报告使用固定的最终结论行, * 避免 pending、blocked 或 changes_required 被一段其他说明文字意外判定为通过。 */ export function getUnpassedReviewFiles(reviewReports) { return REVIEW_FILES.filter((file) => { const content = reviewReports.get(file) ?? ''; return !/^最终结论:passed$/m.test(content); }); }