Files
a-share-analysis/tools/governance/feature-record.mjs
T

164 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
});
}