164 lines
5.2 KiB
JavaScript
164 lines
5.2 KiB
JavaScript
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(/<!--[\s\S]*?-->/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) ?? '';
|
||
const lastNonEmptyLine = content.trim().split(/\r?\n/).at(-1);
|
||
return lastNonEmptyLine !== '最终结论:passed';
|
||
});
|
||
}
|