feat(GOV-010): 强制校验开发质量证据
This commit is contained in:
@@ -9,6 +9,7 @@ const REQUIRED_FEATURE_FILES = Object.freeze([
|
|||||||
'design.md',
|
'design.md',
|
||||||
'implementation-plan.md',
|
'implementation-plan.md',
|
||||||
'test-plan.md',
|
'test-plan.md',
|
||||||
|
'development-summary.md',
|
||||||
'gitnexus/before.md',
|
'gitnexus/before.md',
|
||||||
'gitnexus/impact-plan.md',
|
'gitnexus/impact-plan.md',
|
||||||
'gitnexus/detected-changes.md',
|
'gitnexus/detected-changes.md',
|
||||||
@@ -27,6 +28,28 @@ const REVIEW_FILES = Object.freeze([
|
|||||||
'review/standards-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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 功能编号是需求、设计、代码提交、测试结果和发布记录之间的主关联键。
|
* 功能编号是需求、设计、代码提交、测试结果和发布记录之间的主关联键。
|
||||||
* 严格限制格式可以避免路径穿越、临时名称以及后续无法检索的模糊编号。
|
* 严格限制格式可以避免路径穿越、临时名称以及后续无法检索的模糊编号。
|
||||||
@@ -58,6 +81,75 @@ export function getRequiredFeatureFiles() {
|
|||||||
return [...REQUIRED_FEATURE_FILES];
|
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 报告不能只靠“文件存在”通过门禁。这里要求报告使用固定的最终结论行,
|
* Review 报告不能只靠“文件存在”通过门禁。这里要求报告使用固定的最终结论行,
|
||||||
* 避免 pending、blocked 或 changes_required 被一段其他说明文字意外判定为通过。
|
* 避免 pending、blocked 或 changes_required 被一段其他说明文字意外判定为通过。
|
||||||
|
|||||||
@@ -2,12 +2,123 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
buildImpactPlanTemplate,
|
||||||
getUnpassedReviewFiles,
|
getUnpassedReviewFiles,
|
||||||
getFeatureDirectory,
|
getFeatureDirectory,
|
||||||
|
getInvalidFeatureEvidence,
|
||||||
getRequiredFeatureFiles,
|
getRequiredFeatureFiles,
|
||||||
validateFeatureId,
|
validateFeatureId,
|
||||||
} from './feature-record.mjs';
|
} from './feature-record.mjs';
|
||||||
|
|
||||||
|
const VALID_FEATURE_REPORTS = new Map([
|
||||||
|
[
|
||||||
|
'development-summary.md',
|
||||||
|
`# 开发总结
|
||||||
|
|
||||||
|
## 原始开发需求
|
||||||
|
|
||||||
|
建立可自动执行的质量门禁。
|
||||||
|
|
||||||
|
## 需求与范围变化
|
||||||
|
|
||||||
|
范围未变化,仍只处理治理与请求脚手架。
|
||||||
|
|
||||||
|
## 实际完成需求
|
||||||
|
|
||||||
|
完成独立总结和证据校验。
|
||||||
|
|
||||||
|
## 修复缺陷
|
||||||
|
|
||||||
|
本次没有修复既有业务缺陷,只补足治理能力。
|
||||||
|
|
||||||
|
## 未完成与延期项
|
||||||
|
|
||||||
|
AI Provider 延期到独立功能,不属于本次范围。
|
||||||
|
|
||||||
|
## 关键提交
|
||||||
|
|
||||||
|
记录中文提交 SHA 与摘要。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
记录测试、构建和 Review 结果。
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'design.md',
|
||||||
|
`# 设计
|
||||||
|
|
||||||
|
## 8. 安全策略与评估
|
||||||
|
|
||||||
|
说明信任边界和残余风险。
|
||||||
|
|
||||||
|
## 9. 性能目标与评估
|
||||||
|
|
||||||
|
说明预算和验证强度。
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'test-plan.md',
|
||||||
|
`# 测试
|
||||||
|
|
||||||
|
## 安全验证
|
||||||
|
|
||||||
|
验证脱敏错误。
|
||||||
|
|
||||||
|
## 性能验证
|
||||||
|
|
||||||
|
比较构建体积。
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gitnexus/impact-plan.md',
|
||||||
|
`# 影响
|
||||||
|
|
||||||
|
## 直接与间接影响
|
||||||
|
|
||||||
|
列出消费者和执行流。
|
||||||
|
|
||||||
|
## 跨模块同步修改
|
||||||
|
|
||||||
|
逐项判断契约、缓存和进程。
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'review/code-review.md',
|
||||||
|
`# Review
|
||||||
|
|
||||||
|
## 安全与残余风险
|
||||||
|
|
||||||
|
错误信息经过脱敏,残余风险已记录。
|
||||||
|
|
||||||
|
## 性能与资源影响
|
||||||
|
|
||||||
|
包体增量经过测量并可接受。
|
||||||
|
|
||||||
|
## GitNexus 与同步修改
|
||||||
|
|
||||||
|
实际影响符合开发前计划。
|
||||||
|
|
||||||
|
最终结论:passed
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'review/standards-review.md',
|
||||||
|
`# 规范 Review
|
||||||
|
|
||||||
|
## 安全、性能与架构检查
|
||||||
|
|
||||||
|
逐项核对并符合规范。
|
||||||
|
|
||||||
|
## GitNexus 与开发总结检查
|
||||||
|
|
||||||
|
前后影响和完成事实证据齐全。
|
||||||
|
|
||||||
|
最终结论:passed
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
test('接受带业务域和三位序号的功能编号', () => {
|
test('接受带业务域和三位序号的功能编号', () => {
|
||||||
assert.equal(validateFeatureId('FEAT-MO-001'), true);
|
assert.equal(validateFeatureId('FEAT-MO-001'), true);
|
||||||
assert.equal(validateFeatureId('GOV-001'), true);
|
assert.equal(validateFeatureId('GOV-001'), true);
|
||||||
@@ -34,6 +145,7 @@ test('返回开发完成前必须存在的完整追溯文件清单', () => {
|
|||||||
'design.md',
|
'design.md',
|
||||||
'implementation-plan.md',
|
'implementation-plan.md',
|
||||||
'test-plan.md',
|
'test-plan.md',
|
||||||
|
'development-summary.md',
|
||||||
'gitnexus/before.md',
|
'gitnexus/before.md',
|
||||||
'gitnexus/impact-plan.md',
|
'gitnexus/impact-plan.md',
|
||||||
'gitnexus/detected-changes.md',
|
'gitnexus/detected-changes.md',
|
||||||
@@ -64,3 +176,89 @@ test('只有两份 Review 都明确通过时才允许发布', () => {
|
|||||||
);
|
);
|
||||||
assert.deepEqual(getUnpassedReviewFiles(reports), []);
|
assert.deepEqual(getUnpassedReviewFiles(reports), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('完整质量证据和带序号的二级标题可以通过门禁', () => {
|
||||||
|
assert.deepEqual(getInvalidFeatureEvidence(VALID_FEATURE_REPORTS), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('一次列出缺失、空白和无解释占位的全部证据章节', () => {
|
||||||
|
const reports = new Map(VALID_FEATURE_REPORTS);
|
||||||
|
reports.set(
|
||||||
|
'development-summary.md',
|
||||||
|
`# 开发总结
|
||||||
|
|
||||||
|
## 原始开发需求
|
||||||
|
|
||||||
|
建立质量门禁。
|
||||||
|
|
||||||
|
## 需求与范围变化
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
## 实际完成需求
|
||||||
|
|
||||||
|
完成结构校验。
|
||||||
|
|
||||||
|
## 修复缺陷
|
||||||
|
|
||||||
|
N/A
|
||||||
|
|
||||||
|
## 未完成与延期项
|
||||||
|
|
||||||
|
|
||||||
|
## 关键提交
|
||||||
|
|
||||||
|
记录提交。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
记录测试。
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
reports.set(
|
||||||
|
'test-plan.md',
|
||||||
|
`# 测试
|
||||||
|
|
||||||
|
## 安全验证
|
||||||
|
|
||||||
|
验证脱敏错误。
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(getInvalidFeatureEvidence(reports), [
|
||||||
|
'development-summary.md:需求与范围变化',
|
||||||
|
'development-summary.md:修复缺陷',
|
||||||
|
'development-summary.md:未完成与延期项',
|
||||||
|
'test-plan.md:性能验证',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('带具体理由和边界的不适用说明不是空壳证据', () => {
|
||||||
|
const reports = new Map(VALID_FEATURE_REPORTS);
|
||||||
|
reports.set(
|
||||||
|
'test-plan.md',
|
||||||
|
`# 测试
|
||||||
|
|
||||||
|
## 安全验证
|
||||||
|
|
||||||
|
验证脱敏错误。
|
||||||
|
|
||||||
|
## 性能验证
|
||||||
|
|
||||||
|
不适用:本功能不进入生产请求路径,边界仅为开发门禁。
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(getInvalidFeatureEvidence(reports), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('开发前影响计划骨架要求记录风险、消费者和跨模块同步修改', () => {
|
||||||
|
const template = buildImpactPlanTemplate('FEAT-MO-001');
|
||||||
|
|
||||||
|
assert.match(template, /功能编号:FEAT-MO-001/);
|
||||||
|
assert.match(template, /^## 直接与间接影响$/m);
|
||||||
|
assert.match(template, /query、context、upstream impact、消费者和执行流/);
|
||||||
|
assert.match(template, /^## 跨模块同步修改$/m);
|
||||||
|
assert.match(template, /HTTP\/OpenAPI、Web API、消息契约、数据库、Redis/);
|
||||||
|
assert.match(template, /API、Scheduler、Worker、配置部署、测试和文档/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getFeatureDirectory,
|
getFeatureDirectory,
|
||||||
|
getInvalidFeatureEvidence,
|
||||||
getRequiredFeatureFiles,
|
getRequiredFeatureFiles,
|
||||||
getUnpassedReviewFiles,
|
getUnpassedReviewFiles,
|
||||||
validateFeatureId,
|
validateFeatureId,
|
||||||
@@ -65,10 +66,24 @@ if (missingFiles.length > 0) {
|
|||||||
throw new Error(`完成门禁未通过,缺少或为空:\n- ${missingFiles.join('\n- ')}`);
|
throw new Error(`完成门禁未通过,缺少或为空:\n- ${missingFiles.join('\n- ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const featureReports = new Map(
|
||||||
|
getRequiredFeatureFiles().map((relativeFile) => [
|
||||||
|
relativeFile,
|
||||||
|
readFileSync(path.join(featureDirectory, relativeFile), 'utf8'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const invalidEvidence = getInvalidFeatureEvidence(featureReports);
|
||||||
|
|
||||||
|
if (invalidEvidence.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`完成门禁未通过,证据章节缺失或说明无效:\n- ${invalidEvidence.join('\n- ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const reviewReports = new Map(
|
const reviewReports = new Map(
|
||||||
['review/code-review.md', 'review/standards-review.md'].map((relativeFile) => [
|
['review/code-review.md', 'review/standards-review.md'].map((relativeFile) => [
|
||||||
relativeFile,
|
relativeFile,
|
||||||
readFileSync(path.join(featureDirectory, relativeFile), 'utf8'),
|
featureReports.get(relativeFile),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
const unpassedReviews = getUnpassedReviewFiles(reviewReports);
|
const unpassedReviews = getUnpassedReviewFiles(reviewReports);
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { assertCurrentReleasedBaselinePublished } from '../database/schema-baseline.mjs';
|
import { assertCurrentReleasedBaselinePublished } from '../database/schema-baseline.mjs';
|
||||||
import { getFeatureDirectory, validateFeatureId } from './feature-record.mjs';
|
import {
|
||||||
|
buildImpactPlanTemplate,
|
||||||
|
getFeatureDirectory,
|
||||||
|
validateFeatureId,
|
||||||
|
} from './feature-record.mjs';
|
||||||
|
|
||||||
const [featureId, slug] = process.argv.slice(2);
|
const [featureId, slug] = process.argv.slice(2);
|
||||||
const repositoryRoot = process.cwd();
|
const repositoryRoot = process.cwd();
|
||||||
@@ -15,13 +19,15 @@ if (!featureId || !slug || !validateFeatureId(featureId)) {
|
|||||||
const featureDirectory = getFeatureDirectory(repositoryRoot, featureId, slug);
|
const featureDirectory = getFeatureDirectory(repositoryRoot, featureId, slug);
|
||||||
const gitnexusDirectory = path.join(featureDirectory, 'gitnexus');
|
const gitnexusDirectory = path.join(featureDirectory, 'gitnexus');
|
||||||
const beforeReport = path.join(gitnexusDirectory, 'before.md');
|
const beforeReport = path.join(gitnexusDirectory, 'before.md');
|
||||||
|
const impactPlan = path.join(gitnexusDirectory, 'impact-plan.md');
|
||||||
|
|
||||||
// Schema 基线必须在新版本的功能档案和 GitNexus 分析之前已经独立提交;
|
// Schema 基线必须在新版本的功能档案和 GitNexus 分析之前已经独立提交;
|
||||||
// 这里仅做只读验证,绝不在一般功能开发命令中连接数据库或静默生成历史产物。
|
// 这里仅做只读验证,绝不在一般功能开发命令中连接数据库或静默生成历史产物。
|
||||||
assertCurrentReleasedBaselinePublished(repositoryRoot);
|
assertCurrentReleasedBaselinePublished(repositoryRoot);
|
||||||
|
|
||||||
if (existsSync(beforeReport)) {
|
const existingReports = [beforeReport, impactPlan].filter((file) => existsSync(file));
|
||||||
throw new Error(`开发前报告已存在,为保护历史不会覆盖:${beforeReport}`);
|
if (existingReports.length > 0) {
|
||||||
|
throw new Error(`开发前报告已存在,为保护历史不会覆盖:${existingReports.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(gitnexusDirectory, { recursive: true });
|
mkdirSync(gitnexusDirectory, { recursive: true });
|
||||||
@@ -64,5 +70,6 @@ writeFileSync(
|
|||||||
`\`gitnexus/impact-plan.md\`。高风险结果必须先获得用户确认。\n`,
|
`\`gitnexus/impact-plan.md\`。高风险结果必须先获得用户确认。\n`,
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
|
writeFileSync(impactPlan, buildImpactPlanTemplate(featureId), 'utf8');
|
||||||
|
|
||||||
process.stdout.write(`已创建开发前基线:${beforeReport}\n`);
|
process.stdout.write(`已创建开发前基线:${beforeReport}\n已创建影响计划:${impactPlan}\n`);
|
||||||
|
|||||||
Reference in New Issue
Block a user