feat(GOV-002): 增加 PRD 与开发审查门禁
This commit is contained in:
@@ -4,6 +4,7 @@ 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',
|
||||
@@ -15,10 +16,17 @@ const REQUIRED_FEATURE_FILES = Object.freeze([
|
||||
'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',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 功能编号是需求、设计、代码提交、测试结果和发布记录之间的主关联键。
|
||||
* 严格限制格式可以避免路径穿越、临时名称以及后续无法检索的模糊编号。
|
||||
@@ -49,3 +57,14 @@ export function getFeatureDirectory(repositoryRoot, 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
getUnpassedReviewFiles,
|
||||
getFeatureDirectory,
|
||||
getRequiredFeatureFiles,
|
||||
validateFeatureId,
|
||||
@@ -28,6 +29,7 @@ test('将功能编号和短名称解析到固定的功能档案目录', () => {
|
||||
test('返回开发完成前必须存在的完整追溯文件清单', () => {
|
||||
assert.deepEqual(getRequiredFeatureFiles(), [
|
||||
'manifest.yaml',
|
||||
'prd.md',
|
||||
'requirements.md',
|
||||
'design.md',
|
||||
'implementation-plan.md',
|
||||
@@ -39,7 +41,26 @@ test('返回开发完成前必须存在的完整追溯文件清单', () => {
|
||||
'gitnexus/comparison.md',
|
||||
'qa/test-results.md',
|
||||
'qa/visual-qa.md',
|
||||
'review/code-review.md',
|
||||
'review/standards-review.md',
|
||||
'release-notes.md',
|
||||
'rollback.md',
|
||||
]);
|
||||
});
|
||||
|
||||
test('只有两份 Review 都明确通过时才允许发布', () => {
|
||||
const reports = new Map([
|
||||
['review/code-review.md', '# 代码 Review\n\n最终结论:passed\n'],
|
||||
['review/standards-review.md', '# 规范 Review\n\n状态:pending\n'],
|
||||
]);
|
||||
|
||||
assert.deepEqual(getUnpassedReviewFiles(reports), [
|
||||
'review/standards-review.md',
|
||||
]);
|
||||
|
||||
reports.set(
|
||||
'review/standards-review.md',
|
||||
'# 规范 Review\n\n最终结论:passed\n',
|
||||
);
|
||||
assert.deepEqual(getUnpassedReviewFiles(reports), []);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import {
|
||||
getFeatureDirectory,
|
||||
getRequiredFeatureFiles,
|
||||
getUnpassedReviewFiles,
|
||||
validateFeatureId,
|
||||
} from './feature-record.mjs';
|
||||
|
||||
@@ -60,6 +61,20 @@ if (missingFiles.length > 0) {
|
||||
throw new Error(`完成门禁未通过,缺少或为空:\n- ${missingFiles.join('\n- ')}`);
|
||||
}
|
||||
|
||||
const reviewReports = new Map(
|
||||
['review/code-review.md', 'review/standards-review.md'].map((relativeFile) => [
|
||||
relativeFile,
|
||||
readFileSync(path.join(featureDirectory, relativeFile), 'utf8'),
|
||||
]),
|
||||
);
|
||||
const unpassedReviews = getUnpassedReviewFiles(reviewReports);
|
||||
|
||||
if (unpassedReviews.length > 0) {
|
||||
throw new Error(
|
||||
`完成门禁未通过,Review 最终结论不是 passed:\n- ${unpassedReviews.join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`基础门禁已通过。提交前仍须人工确认 GitNexus 影响报告、业务测试、视觉 QA 和回退说明。\n`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user