Files
L2keka/server/src/services/code-quality.ts
T
hangshuo652 1b89743d77 评审真实性增强:前端硬规则+人工启动方案+文档-实现三级校验
- 前端存在性硬规则:detectFrontend 源码级判定,web形态无真实前端/不可访问 → 实现完整度封顶50%(CLI/插件豁免)
- verify 多态:build_status 支持 done/failed/done_no_ui,前端按钮新增「无前端」
- 人工启动方案:评审环境自动启动失败 → awaiting_browse 暂停,评委手动启动后填 service_url 走 /verify-browse 恢复(SSRF 信任评委确认地址)
- Gitea 稳定性:clone 自动重试×3(525抖动)+ 每队独立 token 回退(全局账号失效兜底)
- A 阶段报告备份:feature_inventory.__stageA_report 防 B 阶段维度重复累积/A维度丢失
- tryTest 依赖预装:Python 项目自动 pip install(测试不再因缺依赖误判失败)
- 运行验证压缩静态维度:前端缺失+测试失败 → 静态纸面维度封顶50%(防原型拿高分)
- 文档-实现三级校验:claim-consistency 注入运行时证据(构建/测试/前端),识别「代码有但跑不起来」的虚报(逸飞冲天 61%→25%)
- 赛事文档:中期20%+最终80%(AI=人工总分)、评审表/评审规则/赛事说明/07设计同步
- 清空 review_snapshots 测试污染数据,保留当前正式分(零号158/逸飞冲天126)
- 测试 446 全通过(后端435+前端11)
2026-09-01 08:44:56 +08:00

171 lines
7.6 KiB
TypeScript
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 'path';
/**
* 代码质量确定性评估(2026-08-27):防"光有数量没有质量"。
*
* 数量(行数/功能数/测试数)可以靠复制粘贴/空壳堆出来;质量必须用确定性信号压制。
* 三个核心质量指标:
* 1. 空壳率 stubRatio —— 空函数/TODO/占位占比(数量虚胖的直接证据)
* 2. 重复率 dupRatio —— 复制粘贴占比(灌水代码的直接证据)
* 3. 测试有效度 testValidity —— 测试断言数/核心功能覆盖(只测边角料的测试无价值)
*
* 输出 qualityScore 0~100 + 各指标明细,供"规模与功能点/实现完整度"评分绑定与硬规则。
*/
export interface CodeQualityResult {
applicable: boolean;
// 空壳
stubRatio: number; // 0~1,空壳函数/占位占比
stubHits: { file: string; line: number }[];
// 重复
dupRatio: number; // 0~1,重复代码行占比
// 测试有效度
testAssertionCount: number; // 测试文件中断言总数
testFunctionCount: number; // 测试函数数
testValidityRatio: number; // 0~1,测试有效性(断言数足够、覆盖核心)
qualityScore: number; // 0~100 综合质量分
toPrompt: string;
}
const TODO_RE = /\b(TODO|FIXME|XXX|HACK|not\s*implemented|暂不|待实现|待接入)\b/i;
const STUB_BODY_RE = /pass\b|\.\.\.|throw\s+new\s+Error\(['"]not\s*implemented|return\s+null\s*;?\s*$/;
/**
* 从文件列表计算代码质量指标。
*/
export function computeCodeQuality(files: { path: string; content?: string }[]): CodeQualityResult {
const empty: CodeQualityResult = {
applicable: false, stubRatio: 0, stubHits: [], dupRatio: 0,
testAssertionCount: 0, testFunctionCount: 0, testValidityRatio: 0,
qualityScore: 0, toPrompt: '',
};
const code = (files || []).filter(f =>
/\.(ts|tsx|js|jsx|mjs|cjs|py|java|go|rs)$/i.test(f.path) &&
typeof f.content === 'string' &&
!/node_modules|dist|build|__pycache__|\.d\.ts$|coverage/i.test(f.path));
if (code.length === 0) return empty;
empty.applicable = true;
// ---- 1. 空壳率 ----
let totalFuncs = 0, stubFuncs = 0;
const stubHits: { file: string; line: number }[] = [];
let todoCount = 0;
for (const f of code) {
const lines = f.content!.split('\n');
const isPy = /\.py$/i.test(f.path);
const isTS = /\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(f.path);
// 函数定义检测(语言相关)
let defStart = -1;
for (let i = 0; i < lines.length; i++) {
const l = lines[i].trim();
if (isPy) {
const m = l.match(/^\s*def\s+(\w+)\s*\(/);
if (m) {
totalFuncs++;
defStart = i;
// 检查后续几行是否空壳(pass / return None / docstring-only
let isStub = false;
for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
const inner = lines[j].trim();
if (inner.startsWith('"""') || inner.startsWith("'''") || inner.startsWith('#')) continue;
if (!inner) continue;
if (/^\s*(pass|\.\.\.|return\s*(None|''|"")|raise\s+NotImplementedError|raise\s+NotImplementedException)\s*$/.test(inner)) {
isStub = true;
}
break;
}
if (isStub) { stubFuncs++; stubHits.push({ file: f.path, line: i + 1 }); }
}
} else if (isTS) {
const m = l.match(/^(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(|^const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>|^export\s+const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/);
if (m) {
totalFuncs++;
// 检查函数体是否空({ } 内无内容,或直接 return 字面量)
let bodyStart = -1;
for (let j = i; j < Math.min(i + 6, lines.length); j++) {
if (lines[j].includes('{')) { bodyStart = j; break; }
}
if (bodyStart >= 0) {
const rest = lines.slice(bodyStart).join('\n').slice(0, 120);
if (/^\s*\{\s*\}/.test(rest) || /=>\s*\{\s*\}\s*;?$/.test(rest)) {
stubFuncs++; stubHits.push({ file: f.path, line: i + 1 });
}
}
}
} else if (/\.(java|go|rs)$/i.test(f.path)) {
// Java/Go/Rust: func|fn|public 方法
const m = l.match(/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:func\s+\w+|fn\s+\w+|[\w<>,\[\] ]+\s+\w+\s*\()/);
if (m) {
totalFuncs++;
const rest = lines.slice(i).join('\n').slice(0, 200);
if (/\{\s*\}/.test(rest) || /\{\s*panic!?\("not implemented|return\s+nil\s*$/.test(rest)) {
stubFuncs++; stubHits.push({ file: f.path, line: i + 1 });
}
}
}
if (TODO_RE.test(l)) todoCount++;
}
}
const stubRatio = totalFuncs > 0 ? stubFuncs / totalFuncs : 0;
// ---- 2. 重复率(行级归一化 MD5----
const md5 = new Map<string, number>();
let hashTotal = 0, dupLines = 0;
for (const f of code) {
const lines = f.content!.split('\n').map(l => l.trim()).filter(l => l.length > 25 && !l.startsWith('//') && !l.startsWith('#'));
for (const l of lines) {
const h = require('crypto').createHash('md5').update(l).digest('hex');
md5.set(h, (md5.get(h) || 0) + 1);
hashTotal++;
}
}
for (const cnt of md5.values()) if (cnt > 1) dupLines += cnt;
const dupRatio = hashTotal > 0 ? dupLines / hashTotal : 0;
// ---- 3. 测试有效度 ----
const testFiles = code.filter(f => /(^|[\\/])(test|tests|spec|__tests__)[\\/_.-]|\.test\.|\.spec\./i.test(f.path));
let assertions = 0, testFuncs = 0;
for (const f of testFiles) {
const c = f.content!;
assertions += (c.match(/\b(assert|expect|assertThat|assertTrue|assertEquals|assertEqual|self\.assert)\b|\.to(Be|Equal|Match|Contain)|assert\./g) || []).length;
testFuncs += (c.match(/\b(def\s+test_|it\(|test\(|@Test|func\s+Test)/g) || []).length;
}
// 测试有效度:每个测试函数平均有断言才有效(且覆盖率信息由 tryTest 提供)
const testValidityRatio = testFuncs > 0 ? Math.min(1, assertions / (testFuncs * 2)) : (testFiles.length > 0 ? 0.3 : 0);
// ---- 综合质量分(0~100----
// 空壳率权重 40、重复率权重 30、测试有效度权重 30
const stubScore = Math.max(0, 40 * (1 - stubRatio * 4)); // stub>25% → 0
const dupScore = Math.max(0, 30 * (1 - dupRatio * 2)); // dup>50% → 0
const testScore = 30 * testValidityRatio;
const qualityScore = Math.round(stubScore + dupScore + testScore);
// ---- 输出文本 ----
const pct = (n: number) => Math.round(n * 100) + '%';
const lines: string[] = [
`代码质量确定性评估(${code.length} 个源码文件)`,
`- 空壳率:${pct(stubRatio)}${stubFuncs}/${totalFuncs} 函数为占位/空壳,密度 ${todoCount} 处 TODO${stubRatio > 0.15 ? ' ⚠️偏高,存在数量虚胖风险' : ''}`,
`- 重复率:${pct(dupRatio)}(行级 MD5 归一化)${dupRatio > 0.3 ? ' ⚠️偏高,存在灌水风险' : ''}`,
`- 测试有效度:${pct(testValidityRatio)}${testFiles.length} 个测试文件,${assertions} 个断言 / ${testFuncs} 个测试函数)${testValidityRatio < 0.5 ? ' ⚠️测试偏弱,可能只测边角料' : ''}`,
`- 质量分:${qualityScore}/100(空壳率40 + 重复率30 + 测试有效度30)`,
];
if (stubHits.length > 0) {
lines.push(`- 空壳函数示例:${stubHits.slice(0, 5).map(h => `${h.file}:${h.line}`).join(', ')}`);
}
return {
applicable: true,
stubRatio,
stubHits: stubHits.slice(0, 10),
dupRatio,
testAssertionCount: assertions,
testFunctionCount: testFuncs,
testValidityRatio,
qualityScore,
toPrompt: '\n## 代码质量评估(确定性证据)\n' + lines.join('\n'),
};
}