From e12c57222b571cda4e6b1745e104e46184e94aa4 Mon Sep 17 00:00:00 2001 From: Citrons <909229731@qq.com> Date: Thu, 13 Aug 2026 22:47:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(GOV-010):=20=E5=BC=BA=E5=88=B6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=BC=80=E5=8F=91=E8=B4=A8=E9=87=8F=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/governance/feature-record.mjs | 92 +++++++++++ tools/governance/feature-record.test.mjs | 198 +++++++++++++++++++++++ tools/governance/finish.mjs | 17 +- tools/governance/start.mjs | 15 +- 4 files changed, 317 insertions(+), 5 deletions(-) diff --git a/tools/governance/feature-record.mjs b/tools/governance/feature-record.mjs index 1fb8059..b3ffb86 100644 --- a/tools/governance/feature-record.mjs +++ b/tools/governance/feature-record.mjs @@ -9,6 +9,7 @@ const REQUIRED_FEATURE_FILES = Object.freeze([ 'design.md', 'implementation-plan.md', 'test-plan.md', + 'development-summary.md', 'gitnexus/before.md', 'gitnexus/impact-plan.md', 'gitnexus/detected-changes.md', @@ -27,6 +28,28 @@ const REVIEW_FILES = Object.freeze([ '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]; } +/** + * 二级标题允许带文档章节序号,但证据名称必须完整匹配,避免靠散文中偶然出现的 + * “安全”或“性能”字样绕过门禁。正文只剔除空行和 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 被一段其他说明文字意外判定为通过。 diff --git a/tools/governance/feature-record.test.mjs b/tools/governance/feature-record.test.mjs index abc9abb..ebc40c0 100644 --- a/tools/governance/feature-record.test.mjs +++ b/tools/governance/feature-record.test.mjs @@ -2,12 +2,123 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + buildImpactPlanTemplate, getUnpassedReviewFiles, getFeatureDirectory, + getInvalidFeatureEvidence, getRequiredFeatureFiles, validateFeatureId, } 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('接受带业务域和三位序号的功能编号', () => { assert.equal(validateFeatureId('FEAT-MO-001'), true); assert.equal(validateFeatureId('GOV-001'), true); @@ -34,6 +145,7 @@ test('返回开发完成前必须存在的完整追溯文件清单', () => { 'design.md', 'implementation-plan.md', 'test-plan.md', + 'development-summary.md', 'gitnexus/before.md', 'gitnexus/impact-plan.md', 'gitnexus/detected-changes.md', @@ -64,3 +176,89 @@ test('只有两份 Review 都明确通过时才允许发布', () => { ); 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、配置部署、测试和文档/); +}); diff --git a/tools/governance/finish.mjs b/tools/governance/finish.mjs index 161fff2..e3f506c 100644 --- a/tools/governance/finish.mjs +++ b/tools/governance/finish.mjs @@ -4,6 +4,7 @@ import path from 'node:path'; import { getFeatureDirectory, + getInvalidFeatureEvidence, getRequiredFeatureFiles, getUnpassedReviewFiles, validateFeatureId, @@ -65,10 +66,24 @@ if (missingFiles.length > 0) { 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( ['review/code-review.md', 'review/standards-review.md'].map((relativeFile) => [ relativeFile, - readFileSync(path.join(featureDirectory, relativeFile), 'utf8'), + featureReports.get(relativeFile), ]), ); const unpassedReviews = getUnpassedReviewFiles(reviewReports); diff --git a/tools/governance/start.mjs b/tools/governance/start.mjs index 9782f56..664f84a 100644 --- a/tools/governance/start.mjs +++ b/tools/governance/start.mjs @@ -3,7 +3,11 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; 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 repositoryRoot = process.cwd(); @@ -15,13 +19,15 @@ if (!featureId || !slug || !validateFeatureId(featureId)) { const featureDirectory = getFeatureDirectory(repositoryRoot, featureId, slug); const gitnexusDirectory = path.join(featureDirectory, 'gitnexus'); const beforeReport = path.join(gitnexusDirectory, 'before.md'); +const impactPlan = path.join(gitnexusDirectory, 'impact-plan.md'); // Schema 基线必须在新版本的功能档案和 GitNexus 分析之前已经独立提交; // 这里仅做只读验证,绝不在一般功能开发命令中连接数据库或静默生成历史产物。 assertCurrentReleasedBaselinePublished(repositoryRoot); -if (existsSync(beforeReport)) { - throw new Error(`开发前报告已存在,为保护历史不会覆盖:${beforeReport}`); +const existingReports = [beforeReport, impactPlan].filter((file) => existsSync(file)); +if (existingReports.length > 0) { + throw new Error(`开发前报告已存在,为保护历史不会覆盖:${existingReports.join(', ')}`); } mkdirSync(gitnexusDirectory, { recursive: true }); @@ -64,5 +70,6 @@ writeFileSync( `\`gitnexus/impact-plan.md\`。高风险结果必须先获得用户确认。\n`, 'utf8', ); +writeFileSync(impactPlan, buildImpactPlanTemplate(featureId), 'utf8'); -process.stdout.write(`已创建开发前基线:${beforeReport}\n`); +process.stdout.write(`已创建开发前基线:${beforeReport}\n已创建影响计划:${impactPlan}\n`);