- 前端存在性硬规则: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)
171 lines
7.6 KiB
TypeScript
171 lines
7.6 KiB
TypeScript
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'),
|
||
};
|
||
}
|