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(); 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'), }; }