guanghulab/glada/code-generator.js
2026-05-10 13:12:44 +08:00

211 lines
5.8 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.

/**
* GLADA · 代码生成器 + 回归防护 · code-generator.js
*
* 解决"修了这个坏那个"的问题:
* 1. 修改前,自动拍摄所有相关文件的快照
* 2. 修改后,自动运行现有测试
* 3. 测试失败则自动回滚并尝试其他方案
* 4. 记录所有文件的依赖关系,确保修改不破坏依赖方
*
* 版权:国作登字-2026-A-00037559
* 签发:铸渊 · ICE-GL-ZY001
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..');
/**
* 查找文件的所有被引用方(谁依赖了这个文件)
* @param {string} targetRelPath - 被修改的文件(相对路径)
* @returns {string[]} 引用了该文件的文件列表
*/
function findDependents(targetRelPath) {
const dependents = [];
const normalizedTarget = targetRelPath.replace(/\.js$/, '');
// 使用 grep 搜索引用sanitize 输入防止 shell 注入)
try {
const safeBasename = path.basename(normalizedTarget).replace(/[^A-Za-z0-9._-]/g, '');
if (!safeBasename) return dependents;
const grepResult = execSync(
`grep -rl "${safeBasename}" --include="*.js" "${ROOT}" 2>/dev/null || true`,
{ encoding: 'utf-8', timeout: 15000 }
);
const lines = grepResult.trim().split('\n').filter(Boolean);
for (const line of lines) {
const relPath = path.relative(ROOT, line);
// 排除 node_modules 和自身
if (!relPath.startsWith('node_modules') && relPath !== targetRelPath) {
dependents.push(relPath);
}
}
} catch {
// grep 失败不影响主流程
}
return dependents.slice(0, 20); // 限制数量
}
/**
* 回归防护检查
* 验证修改后的文件不影响其依赖方
* @param {string[]} changedFiles - 已修改的文件列表
* @returns {{ safe: boolean, issues: string[] }}
*/
function regressionCheck(changedFiles) {
const issues = [];
for (const filePath of changedFiles) {
const dependents = findDependents(filePath);
if (dependents.length > 0) {
// 检查依赖方是否能正常 require/import
for (const dep of dependents.slice(0, 5)) {
const absPath = path.resolve(ROOT, dep);
try {
// 简单的语法检查
execSync(`node --check "${absPath}" 2>&1`, {
encoding: 'utf-8',
timeout: 5000
});
} catch (err) {
issues.push(`${dep} 语法检查失败 (依赖 ${filePath}): ${err.message.substring(0, 200)}`);
}
}
}
}
return {
safe: issues.length === 0,
issues
};
}
/**
* 带回归防护的代码变更执行器
* @param {Object[]} files - 文件变更列表
* @param {Object} constraints - 约束条件
* @param {Object} [options] - 选项
* @returns {Promise<Object>} 执行结果
*/
async function executeWithRegressionGuard(files, constraints, options = {}) {
const result = {
success: false,
applied: [],
rolled_back: false,
regression_check: null,
test_result: null,
errors: []
};
// 1. 收集所有要修改的文件路径
const filePaths = files.map(f => f.path);
// 2. 拍摄快照
const snapshot = new Map();
for (const relPath of filePaths) {
const absPath = path.resolve(ROOT, relPath);
if (fs.existsSync(absPath)) {
snapshot.set(relPath, fs.readFileSync(absPath, 'utf-8'));
} else {
snapshot.set(relPath, null);
}
}
// 3. 检查约束
const noTouch = constraints?.no_touch_files || [];
const safeFiles = files.filter(f => {
const blocked = noTouch.some(nt => f.path.startsWith(nt));
if (blocked) {
result.errors.push(`跳过受保护文件: ${f.path}`);
}
return !blocked;
});
// 4. 应用变更
for (const file of safeFiles) {
const absPath = path.resolve(ROOT, file.path);
try {
if (file.action === 'delete') {
if (fs.existsSync(absPath)) fs.unlinkSync(absPath);
} else {
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, file.content, 'utf-8');
}
result.applied.push(file.path);
} catch (err) {
result.errors.push(`${file.action} ${file.path}: ${err.message}`);
}
}
// 5. 回归防护检查
if (result.applied.length > 0) {
result.regression_check = regressionCheck(result.applied);
if (!result.regression_check.safe && options.rollbackOnRegression !== false) {
console.warn('[GLADA-CodeGen] ⚠️ 回归检查发现问题,回滚...');
rollback(snapshot);
result.rolled_back = true;
result.applied = [];
return result;
}
}
// 6. 运行测试
if (options.runTests !== false) {
try {
execSync('npm run test:smoke 2>&1', {
cwd: ROOT,
encoding: 'utf-8',
timeout: 60000
});
result.test_result = { success: true };
} catch (err) {
result.test_result = {
success: false,
output: (err.stdout || err.message || '').substring(0, 500)
};
if (options.rollbackOnTestFail !== false) {
console.warn('[GLADA-CodeGen] ❌ 测试失败,回滚...');
rollback(snapshot);
result.rolled_back = true;
result.applied = [];
return result;
}
}
}
result.success = result.applied.length > 0 && !result.rolled_back;
return result;
}
/**
* 从快照回滚
* @param {Map<string, string|null>} snapshot
*/
function rollback(snapshot) {
for (const [relPath, content] of snapshot) {
const absPath = path.resolve(ROOT, relPath);
if (content === null) {
if (fs.existsSync(absPath)) fs.unlinkSync(absPath);
} else {
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content, 'utf-8');
}
}
}
module.exports = {
findDependents,
regressionCheck,
executeWithRegressionGuard,
rollback
};