284 lines
7.8 KiB
JavaScript
284 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const REQUIRED_FIELDS = [
|
||
'修改前逻辑',
|
||
'问题证据',
|
||
'本次修改',
|
||
'修改理由',
|
||
'已失败尝试',
|
||
'禁止回退',
|
||
'验证命令',
|
||
'验证结果',
|
||
'当前验证范围',
|
||
'遗留风险',
|
||
'当前状态',
|
||
];
|
||
|
||
function parseArgs(argv) {
|
||
const args = {
|
||
caseDir: 'case',
|
||
changed: [],
|
||
init: false,
|
||
requireEntry: false,
|
||
json: false,
|
||
markdown: false,
|
||
help: false,
|
||
};
|
||
for (let i = 2; i < argv.length; i++) {
|
||
const a = argv[i];
|
||
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i] || '';
|
||
else if (a === '--changed' || a === '--file') args.changed.push(argv[++i] || '');
|
||
else if (a === '--init') args.init = true;
|
||
else if (a === '--require-entry') args.requireEntry = true;
|
||
else if (a === '--json') args.json = true;
|
||
else if (a === '--markdown') args.markdown = true;
|
||
else if (a === '--help' || a === '-h') args.help = true;
|
||
else throw new Error(`未知参数:${a}`);
|
||
}
|
||
return args;
|
||
}
|
||
|
||
function usage() {
|
||
return `用法:
|
||
node scripts/check_change_memory.js --case-dir case --init --markdown
|
||
node scripts/check_change_memory.js --case-dir case --markdown
|
||
node scripts/check_change_memory.js --case-dir case --changed result/src/env/install-env.js --require-entry --markdown
|
||
node scripts/check_change_memory.js --case-dir case --changed result/src/signer/index.js --json
|
||
|
||
说明:检查复杂 case 是否维护 case/notes/代码变更记忆.md,并确认关键源码修改后记录了修改原因、失败尝试、禁止回退、验证范围和遗留风险。`;
|
||
}
|
||
|
||
function normalizeSlash(v) {
|
||
return String(v || '').replace(/\\/g, '/');
|
||
}
|
||
|
||
function exists(p) {
|
||
try { fs.accessSync(p); return true; } catch { return false; }
|
||
}
|
||
|
||
function memoryPath(caseDir) {
|
||
return path.join(path.resolve(caseDir), 'notes', '代码变更记忆.md');
|
||
}
|
||
|
||
function createTemplate() {
|
||
return `# 代码变更记忆
|
||
|
||
本文件由通用代码变更记忆机制维护,用于记录复杂 case 中关键源码修改的事实、原因、失败尝试、禁止回退、验证范围和遗留风险。不要把“当前报错消失”直接写成永久结论。
|
||
|
||
## 使用规则
|
||
|
||
- 修改关键源码前先读取本文件,并搜索文件名、函数名、错误关键词和参数名。
|
||
- 修改关键源码后立即追加一条“变更”记录。
|
||
- 不删除失败记录;如历史记录被新证据替代,新增记录说明替代原因。
|
||
- 状态只能使用:临时修复 / 当前验证通过 / 已失败 / 被替代 / 稳定基线。
|
||
|
||
## 变更 001 - 待填写
|
||
|
||
- 时间:YYYY-MM-DD HH:mm
|
||
- 涉及文件:
|
||
- 涉及函数 / 模块:
|
||
- 当前状态:临时修复
|
||
|
||
### 修改前逻辑
|
||
|
||
待填写。
|
||
|
||
### 问题证据
|
||
|
||
待填写。
|
||
|
||
### 本次修改
|
||
|
||
待填写。
|
||
|
||
### 修改理由
|
||
|
||
待填写。
|
||
|
||
### 已失败尝试
|
||
|
||
待填写。
|
||
|
||
### 禁止回退
|
||
|
||
待填写。
|
||
|
||
### 验证命令
|
||
|
||
\`\`\`bash
|
||
# 待填写
|
||
\`\`\`
|
||
|
||
### 验证结果
|
||
|
||
待填写。
|
||
|
||
### 当前验证范围
|
||
|
||
待填写。
|
||
|
||
### 遗留风险
|
||
|
||
待填写。
|
||
`;
|
||
}
|
||
|
||
function initMemory(file) {
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
if (!exists(file)) {
|
||
fs.writeFileSync(file, createTemplate(), 'utf8');
|
||
return 'created';
|
||
}
|
||
return 'kept';
|
||
}
|
||
|
||
function hasEntry(text) {
|
||
return /^##\s+变更\s+\d+/m.test(text);
|
||
}
|
||
|
||
function fieldMissing(text) {
|
||
return REQUIRED_FIELDS.filter(field => !text.includes(field));
|
||
}
|
||
|
||
function changedMatched(text, changedFile) {
|
||
const normalized = normalizeSlash(changedFile).replace(/^\.\//, '');
|
||
const basename = path.basename(normalized);
|
||
const textSlash = normalizeSlash(text);
|
||
if (normalized && textSlash.includes(normalized)) return true;
|
||
if (basename && textSlash.includes(basename)) return true;
|
||
return false;
|
||
}
|
||
|
||
function check(args) {
|
||
const caseDir = path.resolve(args.caseDir || 'case');
|
||
const file = memoryPath(caseDir);
|
||
const problems = [];
|
||
const warnings = [];
|
||
const actions = [];
|
||
|
||
if (args.init) actions.push({ action: initMemory(file), path: file });
|
||
|
||
if (!exists(file)) {
|
||
problems.push(`未找到通用代码变更记忆文件:${file}`);
|
||
return {
|
||
caseDir,
|
||
memoryFile: file,
|
||
exists: false,
|
||
clean: false,
|
||
actions,
|
||
requiredFields: REQUIRED_FIELDS,
|
||
missingFields: REQUIRED_FIELDS.slice(),
|
||
changed: args.changed,
|
||
matchedChanged: [],
|
||
problems,
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
const text = fs.readFileSync(file, 'utf8');
|
||
const missingFields = fieldMissing(text);
|
||
if (missingFields.length) problems.push(`代码变更记忆缺少必要字段:${missingFields.join('、')}`);
|
||
|
||
const entryExists = hasEntry(text);
|
||
if (args.requireEntry && !entryExists) problems.push('已要求至少一条变更记录,但未发现 “## 变更 001” 这类记录。');
|
||
if (!entryExists) warnings.push('尚未发现正式变更记录;如果已经修改关键源码,请立即追加记录。');
|
||
|
||
const matchedChanged = [];
|
||
for (const changed of args.changed.filter(Boolean)) {
|
||
const matched = changedMatched(text, changed);
|
||
matchedChanged.push({ file: changed, matched });
|
||
if (args.requireEntry && !matched) {
|
||
problems.push(`已修改关键文件但代码变更记忆中未提到该文件:${changed}`);
|
||
} else if (!matched) {
|
||
warnings.push(`代码变更记忆中未提到变更文件:${changed}`);
|
||
}
|
||
}
|
||
|
||
if (/正确方案/.test(text)) {
|
||
warnings.push('发现“正确方案”表述;建议改为“当前验证通过 / 稳定基线”,并写明验证范围和遗留风险。');
|
||
}
|
||
|
||
return {
|
||
caseDir,
|
||
memoryFile: file,
|
||
exists: true,
|
||
clean: problems.length === 0,
|
||
actions,
|
||
requiredFields: REQUIRED_FIELDS,
|
||
missingFields,
|
||
hasEntry: entryExists,
|
||
changed: args.changed,
|
||
matchedChanged,
|
||
problems,
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
function renderMarkdown(result) {
|
||
const lines = [
|
||
'# 通用代码变更记忆检查',
|
||
'',
|
||
`- case 目录:${result.caseDir}`,
|
||
`- 记忆文件:${result.memoryFile}`,
|
||
`- 文件存在:${result.exists ? '是' : '否'}`,
|
||
`- 是否通过:${result.clean ? '是' : '否'}`,
|
||
'',
|
||
];
|
||
|
||
if (result.actions.length) {
|
||
lines.push('## 初始化动作');
|
||
for (const item of result.actions) {
|
||
lines.push(`- ${item.action === 'created' ? '已创建' : '已存在'}:${item.path}`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
lines.push('## 必要字段');
|
||
for (const field of result.requiredFields) {
|
||
lines.push(`- ${field}:${result.missingFields.includes(field) ? '缺失' : '存在'}`);
|
||
}
|
||
lines.push('');
|
||
|
||
if (result.matchedChanged && result.matchedChanged.length) {
|
||
lines.push('## 变更文件命中');
|
||
for (const item of result.matchedChanged) {
|
||
lines.push(`- ${item.file}:${item.matched ? '已记录' : '未记录'}`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
if (result.problems.length) {
|
||
lines.push('## 问题');
|
||
for (const p of result.problems) lines.push(`- ${p}`);
|
||
lines.push('');
|
||
}
|
||
|
||
if (result.warnings.length) {
|
||
lines.push('## 提醒');
|
||
for (const w of result.warnings) lines.push(`- ${w}`);
|
||
lines.push('');
|
||
}
|
||
|
||
return lines.join('\n') + '\n';
|
||
}
|
||
|
||
try {
|
||
const args = parseArgs(process.argv);
|
||
if (args.help) {
|
||
console.log(usage());
|
||
process.exit(0);
|
||
}
|
||
const result = check(args);
|
||
if (args.json) console.log(JSON.stringify(result, null, 2));
|
||
if (args.markdown || !args.json) process.stdout.write(renderMarkdown(result));
|
||
process.exit(result.clean ? 0 : 1);
|
||
} catch (err) {
|
||
console.error(err.message || String(err));
|
||
console.error(usage());
|
||
process.exit(1);
|
||
}
|