评审真实性P1落地:全量代码可见性+Mock信号+AI日志审计+定向追踪+功能发现轮

- code-inventory.ts:全量符号索引,注入实现类维度,AI 可见100%文件清单
- code-signals.ts:代码信号检测器(TODO密度/空壳率/硬编码返回/死导入),修复TODO检测顺序bug
- ai-log-audit.ts:AI日志确定性审计(占位率/git一致率/覆盖广度),占位>50%→≤30%、git一致<30%→≤20%封顶
- standard-utils.ts:isQualitativeDesignDim 定性设计维度豁免C档
- review.service.ts:注入三个模块+定向追踪模板+日志封顶+分支提醒+Map-Reduce功能发现轮
- 预算 15000→40000;新增 review-authenticity.test.ts(14用例)
- 实测:净码特攻b3 81分(118功能全量识别),六边形50~62分(Mock被识别)
This commit is contained in:
hangshuo652
2026-08-27 09:23:02 +08:00
parent c16aa70aae
commit f8b4e9d44d
9 changed files with 963 additions and 7 deletions
@@ -0,0 +1,126 @@
import { describe, it, expect } from 'vitest';
import { extractSignatures, renderInventoryText } from '../services/code-inventory';
import { detectStubSignals } from '../services/code-signals';
import { auditAiUsageLog, renderLogAuditToPrompt } from '../services/ai-log-audit';
import { isQualitativeDesignDim, isPureGainDim } from '../services/standard-utils';
describe('TC-INV · 全量符号索引(2026-08-26', () => {
const mk = (path: string, content: string) => ({ path, content, size: content.length });
it('提取 TS 导出符号', () => {
const sigs = extractSignatures([mk('src/a.ts', 'export function foo() {}\nexport class Bar {}\nexport const x = 1;')]);
expect(sigs).toHaveLength(1);
expect(sigs[0].lang).toBe('ts');
expect(sigs[0].lines).toBe(3);
expect(sigs[0].symbols.length).toBeGreaterThanOrEqual(2);
});
it('提取 Python def/class', () => {
const sigs = extractSignatures([mk('mod.py', 'def calc(x):\n return x\ndef other():\n pass\n')]);
expect(sigs[0].lang).toBe('py');
expect(sigs[0].symbols.some(s => s.includes('def calc'))).toBe(true);
});
it('非文本文件只记 path+lines', () => {
const sigs = extractSignatures([{ path: 'assets/logo.png', size: 100000 }]);
expect(sigs[0].lang).toBe('other');
expect(sigs[0].symbols).toHaveLength(0);
});
it('renderInventoryText 含文件路径与预算截断', () => {
const sigs = extractSignatures([mk('src/a.ts', 'export function f(){}\n'), mk('src/b.ts', 'export function g(){}\n')]);
const txt = renderInventoryText(sigs, 2000);
expect(txt).toContain('src/a.ts');
expect(txt).toContain('仓库文件全量索引');
});
});
describe('TC-SIGNAL · 代码信号检测器(2026-08-26', () => {
it('检测 TODO/占位标记', () => {
const r = detectStubSignals([
{ path: 'src/core.ts', content: '// TODO: 接入真实AI\nconst a = 1;\n'.repeat(5) },
]);
expect(r.applicable).toBe(true);
expect(r.todoHits.length).toBeGreaterThan(0);
});
it('非代码文件 → 不适用', () => {
const r = detectStubSignals([{ path: 'README.md', content: 'hello' }]);
expect(r.applicable).toBe(false);
});
it('硬编码返回被标记(启发式)', () => {
const r = detectStubSignals([
{ path: 'src/service.ts', content: 'export function get() {\n return { result: "ok" };\n}\n'.repeat(3) },
]);
expect(r.hardcodedReturns.length).toBeGreaterThan(0);
});
it('toPrompt 含线索说明', () => {
const r = detectStubSignals([{ path: 'src/a.ts', content: '// TODO: 待接入\nconst x=1;\n'.repeat(10) }]);
expect(r.toPrompt).toContain('代码信号扫描');
});
});
describe('TC-LOGAUDIT · AI 日志审计(2026-08-26', () => {
const realLog = [
'# AI Usage Log',
'| 日期时间 | 范式步骤 | 修改摘要 | 涉及文件 | 使用模型 |',
'|---------|---------|---------|---------|---------|',
'| 2026-07-10 | 需求分析 | 写spec | docs/a.md | deepseek |',
'| 2026-07-11 | 编码实现 | 改src | src/app.ts | deepseek |',
].join('\n');
const placeholderLog = [
'# AI Usage Log',
'| 日期时间 | 范式步骤 | 修改摘要 | 涉及文件 | 使用模型 |',
'|---------|---------|---------|---------|---------|',
'| 2026-08-01 | 待补充 | 待补充 | src/a.ts | - |',
'| 2026-08-02 | 待补充 | 待补充 | src/b.ts | - |',
].join('\n');
it('真实日志:占位率低、覆盖环节识别', () => {
const r = auditAiUsageLog(realLog, ['docs/a.md', 'src/app.ts', 'other.ts']);
expect(r.exists).toBe(true);
expect(r.placeholderRatio).toBeLessThanOrEqual(0.2);
expect(r.gitConsistencyRate).toBeGreaterThanOrEqual(0.5);
expect(r.coveragePhases.length).toBeGreaterThan(0);
});
it('占位日志:占位率高 → 触发封顶提示', () => {
const r = auditAiUsageLog(placeholderLog, ['src/a.ts']);
expect(r.placeholderRatio).toBeGreaterThan(0.5);
const txt = renderLogAuditToPrompt(r);
expect(txt).toContain('占位记录占比超过50%');
});
it('无日志 → 明确警告', () => {
const r = auditAiUsageLog(null, []);
const txt = renderLogAuditToPrompt(r);
expect(txt).toContain('未找到');
});
it('git 一致率低 → 封顶提示', () => {
const r = auditAiUsageLog(realLog, ['unrelated.js', 'foo.js', 'bar.js', 'x.js', 'y.js', 'z.js']);
if (r.consistencyChecked >= 5 && r.gitConsistencyRate != null && r.gitConsistencyRate < 0.3) {
const txt = renderLogAuditToPrompt(r);
expect(txt).toContain('一致率低于30%');
} else {
// 涉及文件不足5个时不强制触发,仅验证不崩溃
expect(r.gitConsistencyRate).not.toBeNull();
}
});
});
describe('TC-DIMCLASS · 判档白名单(2026-08-26', () => {
it('定性设计维度豁免', () => {
expect(isQualitativeDesignDim('提效设计合理性')).toBe(true);
expect(isQualitativeDesignDim('开发范式设计清晰度')).toBe(true);
expect(isQualitativeDesignDim('提效幅度')).toBe(false);
});
it('纯增益维度判定', () => {
expect(isPureGainDim('提效幅度')).toBe(true);
expect(isPureGainDim('效果对比')).toBe(true);
expect(isPureGainDim('提效设计合理性')).toBe(false);
});
});
+108
View File
@@ -0,0 +1,108 @@
/**
* AI 使用日志确定性审计(2026-08-26 评审真实性改进方案 §3.2):
* 解决"有壳无货"日志与真实日志无法区分的问题。
* 三个硬指标:占位率 / git 一致率 / 覆盖广度——全部可计算、可复现,
* 结果注入评审 prompt 并触发维度封顶,AI 只能在证据框定的区间内微调。
*/
export interface LogAuditResult {
exists: boolean;
lineCount: number;
recordCount: number; // 表格数据行数(近似记录条数)
placeholderCount: number;
placeholderRatio: number;
involvedFiles: string[]; // 记录中提到的文件路径(basename 去重)
gitConsistencyRate: number | null; // null=git 信息不可用
consistencyChecked: number;
coveragePhases: string[];
toPrompt: string;
}
const PATH_TOKEN_RE = /[\w./\\-]+\.(ts|tsx|js|jsx|mjs|cjs|py|java|go|rs|vue|html|css|scss|sql|md|yaml|yml|json|sh)\b/gi;
export function auditAiUsageLog(
logContent: string | null | undefined,
gitChangedFiles: string[] | null
): LogAuditResult {
const result: LogAuditResult = {
exists: !!logContent && logContent.trim().length > 0,
lineCount: 0, recordCount: 0, placeholderCount: 0, placeholderRatio: 0,
involvedFiles: [], gitConsistencyRate: null, consistencyChecked: 0,
coveragePhases: [], toPrompt: '',
};
if (!result.exists || !logContent) return result;
const lines = logContent.split('\n');
result.lineCount = lines.length;
const PHASE_KEYS = ['需求', '设计', '编码', '实现', '测试'];
let dataRows = 0, placeholderRows = 0;
const involved = new Set<string>();
for (const raw of lines) {
const l = raw.trim();
if (l.startsWith('#') || /^[-|:\s]+$/.test(l)) continue;
// Markdown 表格数据行 → 视为一条记录
if (l.startsWith('|')) {
const cells = l.split('|').map(c => c.trim()).filter(c => c);
if (cells.length < 2) continue;
dataRows++;
// 占位判定:任一关键单元格为占位词
if (cells.some(c => /^(待补充|待定|TODO|-{1,3}|N\/A)$/i.test(c) || c === '')) placeholderRows++;
// 涉及文件:从所有单元格提取路径 token
for (const cell of cells) {
for (const m of cell.matchAll(PATH_TOKEN_RE)) {
involved.add(m[0].replace(/\\/g, '/').split('/').pop()!.toLowerCase());
}
}
}
// 范式步骤覆盖
for (const ph of PHASE_KEYS) {
if (l.includes(ph) && !result.coveragePhases.includes(ph)) result.coveragePhases.push(ph);
}
}
result.recordCount = dataRows;
result.placeholderRatio = dataRows > 0 ? +(placeholderRows / dataRows).toFixed(2) : 1;
result.involvedFiles = [...involved];
// git 一致率:涉及文件的 basename 是否出现在 git 历史变更集中
if (gitChangedFiles && gitChangedFiles.length > 0) {
const gitSet = new Set(gitChangedFiles.map(f => f.replace(/\\/g, '/').split('/').pop()!.toLowerCase()));
let hit = 0;
for (const f of result.involvedFiles) if (gitSet.has(f)) hit++;
result.consistencyChecked = result.involvedFiles.length;
result.gitConsistencyRate = result.involvedFiles.length > 0
? +(hit / result.involvedFiles.length).toFixed(2) : null;
}
return result;
}
/** 渲染审计结果为注入文本 */
export function renderLogAuditToPrompt(r: LogAuditResult): string {
if (!r.exists) {
return '\n## AI 使用日志确定性审计\n⚠️ 未找到 _AI_USAGE_LOG.md 或文件为空。规范 §8 要求全程留痕,此情况按违规后果处理。';
}
const lines: string[] = ['', '## AI 使用日志确定性审计(系统自动检测,评分必须与此一致)'];
lines.push(`- 日志行数:${r.lineCount},记录条数:${r.recordCount}`);
lines.push(`- 占位记录占比:${Math.round(r.placeholderRatio * 100)}%${r.placeholderCount}/${r.recordCount} 条含"待补充"或空值)`);
if (r.gitConsistencyRate != null) {
lines.push(`- 涉及文件与 git 历史一致率:${Math.round(r.gitConsistencyRate * 100)}%(涉及 ${r.involvedFiles.length} 个文件)`);
} else {
lines.push('- git 一致率:无法计算(仓库无提交历史)');
}
lines.push(`- 范式环节覆盖:${r.coveragePhases.length > 0 ? r.coveragePhases.join('/') : '未识别到环节关键词'}`);
lines.push('');
lines.push('**评分约束**');
if (r.placeholderRatio > 0.5) lines.push('- 占位记录占比超过50%,该维度得分不得高于满分30%');
if (r.gitConsistencyRate != null && r.gitConsistencyRate < 0.3 && r.consistencyChecked >= 5) {
lines.push('- 涉及文件与git历史一致率低于30%,涉嫌日志造假,该维度得分不得高于满分20%');
}
if (lines.length <= 7 + 3) { /* no constraint triggered */ }
return '\n' + lines.join('\n');
}
+109
View File
@@ -0,0 +1,109 @@
import path from 'path';
/**
* 全量代码可见性(2026-08-26 评审真实性改进方案 §3.1):
* 对仓库全部文本文件生成轻量签名索引,让评审 AI 看见 100% 文件清单,
* 替代"前60个文件+15K截断"导致的盲区。
*/
export interface FileSignature {
path: string;
lines: number;
lang: 'ts' | 'js' | 'py' | 'other';
symbols: string[];
}
const CODE_LANG: Record<string, 'ts' | 'js' | 'py'> = {
'.ts': 'ts', '.tsx': 'ts', '.mts': 'ts', '.cts': 'ts',
'.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
'.py': 'py',
};
/** 可提取符号的文本扩展名(其余只记 path+lines */
const TEXT_EXTS = new Set([
...Object.keys(CODE_LANG),
'.json', '.yaml', '.yml', '.md', '.html', '.css', '.scss', '.vue',
'.sql', '.sh', '.java', '.go', '.rs', '.rb', '.php', '.cs', '.kt',
]);
// 各语言的定义行模式(保守匹配,宁缺勿滥)
const SYMBOL_PATTERNS: Record<string, RegExp[]> = {
ts: [
/^\s*(export\s+)?(default\s+)?(async\s+)?function\s+\w+/,
/^\s*(export\s+)?(abstract\s+)?class\s+\w+/,
/^\s*export\s+(const|let|var)\s+\w+\s*=\s*(\(|async|function)/,
/^\s*export\s+(interface|type|enum)\s+\w+/,
/^\s*(public|private|protected|static)?\s*(async\s+)?\w+\s*\([^)]*\)\s*[:{]/,
],
js: [
/^\s*(export\s+)?(default\s+)?(async\s+)?function\s+\w+/,
/^\s*(export\s+)?(class\s+\w+)/,
/^\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(\(|async|function|\w+\s*=>)/,
/^\s*(module\.exports|exports\.\w+)\s*=/
],
py: [
/^\s*def\s+\w+/,
/^\s*class\s+\w+/,
],
};
export function extractSignatures(files: { path: string; content?: string; size?: number }[]): FileSignature[] {
const out: FileSignature[] = [];
for (const f of files) {
const ext = path.extname(f.path).toLowerCase();
const rel = f.path.replace(/\\/g, '/');
// 行数:有内容用内容算,否则用 size 粗估
let lines = 0;
let lang: FileSignature['lang'] = 'other';
const codeLang = CODE_LANG[ext];
if (codeLang) lang = codeLang;
let content: string | undefined;
if ('content' in f && typeof (f as any).content === 'string') {
content = (f as any).content as string;
lines = content.split('\n').length;
} else if (f.size != null) {
lines = Math.max(1, Math.round(f.size / 38)); // 平均 38 字节/行 估算
} else {
lines = 1;
}
if (!TEXT_EXTS.has(ext)) {
out.push({ path: rel, lines, lang: 'other', symbols: [] });
continue;
}
const symbols: string[] = [];
if (content && codeLang) {
const patterns = SYMBOL_PATTERNS[codeLang] || [];
for (const line of content.split('\n')) {
if (symbols.length >= 5) break;
for (const re of patterns) {
if (re.test(line)) {
symbols.push(line.trim().slice(0, 100));
break;
}
}
}
}
out.push({ path: rel, lines, lang, symbols });
}
return out;
}
/** 渲染紧凑索引文本(供注入 prompt)——全量文件,每文件 1~3 行 */
export function renderInventoryText(sigs: FileSignature[], budgetChars = 8000): string {
const lines: string[] = ['', '## 仓库文件全量索引(共 ' + sigs.length + ' 个文件,此清单为完整底账,评分必须覆盖其中核心文件)'];
let used = lines.join('\n').length;
for (const s of sigs) {
const symLine = s.symbols.length ? ' | ' + s.symbols.slice(0, 2).join(' ; ') : '';
const l = `- ${s.path} (${s.lines}${s.lang !== 'other' ? ',' + s.lang : ''})${symLine}`;
if (used + l.length > budgetChars) {
lines.push(`- …(其余 ${sigs.length - sigs.indexOf(s)} 个文件见截断说明,均为外围配置/资源)`);
break;
}
lines.push(l);
used += l.length;
}
return '\n' + lines.join('\n');
}
+150
View File
@@ -0,0 +1,150 @@
import path from 'path';
/**
* 代码信号检测器(2026-08-26 评审真实性改进方案 §3.3):
* 确定性统计信号,定位为"线索"而非"判据"——输出注入实现类维度 prompt,
* 提示 AI 定向核查疑似占位/空壳文件,真假判定权归 AI 追踪 + 人工约谈。
*
* 语言范围:TS/JS/Python(其余语言返回空报告,降级为基础统计)。
*/
export interface StubSignals {
applicable: boolean;
totalFiles: number;
todoDensity: number; // 每千行 TODO/FIXME/未实现 出现次数
todoHits: { file: string; line: number; text: string }[];
stubFiles: string[]; // 空壳率>50% 的代码文件(实现行占比低)
hardcodedReturns: { file: string; line: number }[]; // 业务函数 return 字面量(启发式)
deadImports: { imp: string; file: string }[]; // 声明集成但全文无实际调用的依赖
toPrompt: string;
}
const TODO_RE = /\b(TODO|FIXME|XXX|HACK)\b|暂不|待实现|待接入|not\s*implemented/i;
const STUB_RETURN_RE = /^\s*(return|=>)\s*[\[{"][^`]*[\]}"]?\s*;?\s*$/;
/** 核心目录判定:路径含这些片段的文件视为业务核心(排除配置/测试/脚本) */
const CORE_HINTS = [/^src\//i, /\/services?\/|\/controllers?\/|\/handlers?\/|\/engines?\/|\/core\/|\/analyzers?\//i];
export function detectStubSignals(files: { path: string; content: string }[]): StubSignals {
const empty: StubSignals = {
applicable: false, totalFiles: files.length, todoDensity: 0, todoHits: [],
stubFiles: [], hardcodedReturns: [], deadImports: [],
toPrompt: '',
};
// 只处理 TS/JS/Py
const code = files.filter(f => /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(f.path)
&& !/node_modules|dist|build|__pycache__|\.d\.ts$/i.test(f.path));
if (code.length === 0) return empty;
empty.applicable = true;
const todoHits: StubSignals['todoHits'] = [];
const hardcodedReturns: StubSignals['hardcodedReturns'] = [];
const stubFiles: string[] = [];
let totalLines = 0;
const declaredImps: { imp: string; file: string; used: boolean }[] = [];
const deadImports: { imp: string; file: string }[] = [];
for (const f of code) {
const lines = f.content.split('\n');
const isPy = /\.py$/i.test(f.path);
totalLines += lines.length;
let implLines = 0; // 有实际逻辑的行(分支/循环/调用/赋值计算)
let inBlockComment = false;
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const l = raw.trim();
// 块注释状态机(/* */ 与 Python 三引号简化处理)
if (/^\/\*|^'''|^"""/.test(l)) inBlockComment = !inBlockComment || /^\/\*\*$/.test(l) ? !/^\/\*/.test(l) ? false : true : false;
if (inBlockComment) continue;
// TODO/占位检测须在注释过滤之前——// TODO 本身就是信号
if (TODO_RE.test(l)) {
todoHits.push({ file: f.path, line: i + 1, text: l.slice(0, 80) });
}
if (!l || l.startsWith('//') || l.startsWith('#')) continue;
// 硬编码 return 启发式:return 后紧跟完整字面量且行内无变量名/函数调用
if (/^\s*(return\s+)(\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"])\s*;?\s*$/.test(raw)
&& !/[a-zA-Z]\s*[+\-*/]|\w+\(/.test(raw)) {
hardcodedReturns.push({ file: f.path, line: i + 1 });
}
// 实现行估算
if (/\b(if|for|while|switch|try|catch|except)\b|[=<>!]=|\.push\(|\.map\(|\.filter\(|await\s+\w+\(|return\s+\w/.test(l)) implLines++;
// import 收集(TS/JS
if (!isPy) {
const im = raw.match(/^\s*import\s+.*\s+from\s+['"]([^'"]+)['"]/) || raw.match(/^\s*const\s+\w+\s*=\s*require\(['"]([^'"]+)['"]\)/);
if (im && !im[1].startsWith('.')) declaredImps.push({ imp: im[1], file: f.path, used: false });
}
}
// 空壳文件判定:核心目录 + ≥30 行 + 有逻辑的行 <20%
const coreish = CORE_HINTS.some(re => re.test(f.path.replace(/\\/g, '/')));
if (coreish && lines.length >= 30) {
const implRatio = implLines / lines.length;
if (implRatio < 0.2) stubFiles.push(f.path);
}
}
// 死导入复核:import 声明的包名是否在任一文件的后续使用行出现
for (const di of declaredImps) {
const pkgTail = di.imp.split('/').pop() || di.imp;
const usageRe = new RegExp('[^\'"`]' + pkgTail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\.', 'i');
const usedElsewhere = code.some(f =>
!(f.path === di.file && true) && usageRe.test(f.content)) ||
new RegExp('from\\s+[\'"]' + di.imp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\'"]').test('');
void usedElsewhere;
// 简化:检查除声明行外是否有 `<pkg>.` 使用
const ownerFile = code.find(f => f.path === di.file);
if (ownerFile) {
const withoutDecl = ownerFile.content.split('\n').filter(l => !l.includes(di.imp)).join('\n');
di.used = withoutDecl.includes(pkgTail + '.');
}
if (!di.used) deadImports.push({ imp: di.imp, file: di.file });
}
// 去重(同包多文件声明只报一次)
const seenDead = new Set<string>();
const deadUnique = deadImports.filter((x: { imp: string; file: string }) => {
const k = x.imp;
if (seenDead.has(k)) return false;
seenDead.add(k); return true;
}).slice(0, 8);
const todoDensity = totalLines > 0 ? +(todoHits.length / (totalLines / 1000)).toFixed(1) : 0;
const lines: string[] = [];
lines.push(`代码信号扫描(${code.length} 个代码文件 / ${totalLines} 行,以下为确定性线索,请定向核查后再判定)`);
if (todoHits.length > 0) {
lines.push(`- TODO/FIXME/占位标记:${todoHits.length} 处(密度 ${todoDensity}/千行),示例:`);
for (const h of todoHits.slice(0, 5)) lines.push(` · ${h.file}:${h.line} ${h.text}`);
} else {
lines.push('- TODO/FIXME/占位标记:未发现');
}
if (stubFiles.length > 0) {
lines.push(`- 疑似空壳文件(有逻辑行占比<20%):${stubFiles.slice(0, 6).join(', ')}`);
}
if (deadUnique.length > 0) {
lines.push(`- 声明但未见使用的依赖:${deadUnique.map((x: { imp: string }) => x.imp).join(', ')}`);
}
if (hardcodedReturns.length > 0) {
lines.push(`- 直接返回字面量的位置 ${hardcodedReturns.length} 处(可能是合法常量表,也可能是 Mock 数据——请在追踪调用链时重点甄别):`);
for (const h of hardcodedReturns.slice(0, 4)) lines.push(` · ${h.file}:${h.line}`);
}
return {
applicable: true,
totalFiles: code.length,
todoDensity,
todoHits: todoHits.slice(0, 10),
stubFiles,
hardcodedReturns: hardcodedReturns.slice(0, 10),
deadImports: deadUnique,
toPrompt: '\n## 代码信号扫描结果(确定性线索)\n' + lines.join('\n'),
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
export const REVIEW_CONSTANTS = {
MAX_CONCURRENT: 3,
MAX_OVERVIEW_CHARS: 30000,
MAX_FILE_CHARS_NORMAL: 15000,
MAX_FILE_CHARS_NORMAL: 40000,
MAX_FILE_CHARS_BUILD: 40000,
DUP_CAP_SCORE: 3,
NO_README_CAP: 2,
+209 -5
View File
@@ -12,6 +12,9 @@ import { isPathInside } from '../path-security';
import { applyHardRules, applyTrackHardRules } from './hard-rules';
import { findL2Topic, buildL2FuncContext } from './l2-topics';
import { detectPlagiarism, readRepoFiles, detectCommitBehavior, PlagiarismReport } from './plagiarism-detect';
import { extractSignatures, renderInventoryText } from './code-inventory';
import { detectStubSignals } from './code-signals';
import { auditAiUsageLog, renderLogAuditToPrompt } from './ai-log-audit';
import {
REVIEW_CONSTANTS,
BUILD_SYSTEMS,
@@ -199,6 +202,150 @@ function buildL2ExtraContexts(entry: any): Record<string, string> | undefined {
return { '功能完整性': '\n' + block };
}
/** 收集 git 全部变更文件 basename 集合(用于 AI 日志一致率审计) */
async function collectGitChangedFiles(dir: string): Promise<string[] | null> {
try {
const git = simpleGit(dir);
if (!(await git.checkIsRepo())) return null;
const raw = await git.raw(['log', '--name-only', '--pretty=format:']);
return raw.split('\n').map(l => l.trim()).filter(Boolean);
} catch { return null; }
}
/**
* 审计上下文(2026-08-26):全量符号索引 + 代码信号 + 定向追踪模板 + AI 日志审计。
* 按"维度名 → 注入文本"返回,合并进 extraDimContext 机制。
*/
function buildAuditExtraContexts(
entryId: string,
standardDims: { name: string }[],
files: { path: string; content?: string; size?: number }[],
featureInvText: string
): Record<string, string> {
const ctx: Record<string, string> = {};
// 1. 全量符号索引
const sigs = extractSignatures(files);
const invText = renderInventoryText(sigs);
// 2. 代码信号
const codeFiles = files.filter(f => typeof f.content === 'string') as { path: string; content: string }[];
const stubs = detectStubSignals(codeFiles);
// 3. AI 日志审计
const logFile = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(f.path));
let logAuditPrompt = '';
if (logFile && typeof logFile.content === 'string') {
const la = auditAiUsageLog(logFile.content, null); // git 一致率在管线层另行计算后覆盖
logAuditPrompt = renderLogAuditToPrompt(la);
}
// 4. 组装:定向追踪模板注入实现类维度;日志审计注入 AI 日志维度;符号索引注入所有实现类维度
const IMPL_RE = /功能完整|实现完整|规模|功能点/;
const LOG_RE = /AI.*日志|AI协作/;
for (const dim of standardDims) {
const texts: string[] = [];
if (IMPL_RE.test(dim.name)) {
texts.push(invText);
texts.push(stubs.toPrompt);
if (featureInvText) texts.push(featureInvText);
texts.push(getDirectedTraceTemplate());
}
if (LOG_RE.test(dim.name) && logAuditPrompt) {
texts.push(logAuditPrompt);
}
if (texts.length > 0) ctx[dim.name] = '\n' + texts.join('\n');
}
return ctx;
}
/**
* Map-Reduce 功能发现轮(2026-08-26 P1):对全部源码分块扫描,汇总实际功能清单,
* 注入实现类维度让 AI 对照 README/验收基准逐项核对。失败非致命(返回空串)。
*/
async function discoverFeatureInventory(entryId: string, files: { path: string; content?: string; size?: number }[]): Promise<string> {
try {
const src = files.filter(f => /\.(ts|tsx|js|jsx|mjs|cjs|py|java|go|rs|vue)$/i.test(f.path)
&& typeof f.content === 'string' && !/node_modules|dist|build|__pycache__/i.test(f.path)) as { path: string; content: string }[];
if (src.length === 0) return '';
// 分块:聚合文件,每块 ≤12000 字符,最多 12 块
const CHUNK_MAX = 12000;
const chunks: string[] = [];
let cur = '';
for (const f of src) {
const block = `--- ${f.path} ---\n${f.content}`;
if (cur.length + block.length > CHUNK_MAX) {
if (cur.trim()) chunks.push(cur.trim());
cur = block;
} else {
cur += '\n' + block;
}
if (chunks.length >= 11) { if (cur.trim()) chunks.push(cur.trim()); cur = ''; break; }
}
if (cur.trim() && chunks.length < 12) chunks.push(cur.trim());
if (chunks.length === 0) return '';
const perChunk: string[] = [];
for (let i = 0; i < chunks.length; i++) {
const prompt = `你是代码审计助手。下面是一段项目源码片段(分块 ${i + 1}/${chunks.length})。
列出这段代码实现的所有功能,输出严格JSON数组:
[{"feature":"一句话功能描述","files":["文件路径"],"depth":"real|partial|stub"}]
- depth: real=真实逻辑; partial=部分实现; stub=占位/仅框架(return固定值、空函数体、TODO)
- 只列实际由代码体现的功能,不要臆测
源码:
${chunks[i].slice(0, CHUNK_MAX)}`;
const raw = await callDeepSeek(prompt, 1, 'feature-inventory');
if (raw) perChunk.push(raw.trim());
}
if (perChunk.length === 0) return '';
const mergePrompt = `汇总以下多块源码的功能发现结果,去重合并(同名功能取更深的 depth),输出严格JSON数组:
[{"feature":"一句话功能描述","files":["文件路径"],"depth":"real|partial|stub"}]
各块结果:
${perChunk.map((p, i) => `--- 块${i + 1} ---\n${p}`).join('\n\n')}`;
const merged = await callDeepSeek(mergePrompt, 1, 'feature-inventory-merge');
if (!merged) return '';
try {
const arr = JSON.parse(merged.replace(/```(?:json)?\s*([\s\S]*?)```/g, '$1').trim());
if (!Array.isArray(arr)) return '';
pipeLog(entryId, 'FEATURES', `found ${arr.length} features`);
const lines = arr.map((f: any) => `- ${f.feature} [${f.depth}] (${(f.files || []).join(', ')})`).slice(0, 80);
return '\n## 全量代码功能发现(Map-Reduce 扫描,供对照 README/验收基准逐项核对)\n' + lines.join('\n');
} catch {
return '';
}
} catch (e: any) {
pipeLog(entryId, 'FEATURES', `skipped: ${e.message}`);
return '';
}
}
const DIRECTED_TRACE_TEMPLATE = `
## 定向追踪任务(本维度评分的核心依据)
请先从 README 中提取声称的核心功能清单,然后对每项功能执行以下四步:
1. **定位实现入口**(文件名:行号)
2. **追踪调用链**:入口 → 中间层 → 最终处理逻辑,说明每一层实际做了什么
3. **判定实现深度**:真实 / 部分实现 / 占位(Mock)
- 真实=能对任意合理输入产生正确输出
- 部分=主干通但边界/异常缺失
- 占位=返回固定数据、空逻辑、仅UI无处理
4. **引用代码原文**作为证据(不少于1行)
输出严格JSON
{"checks":[{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub","evidence":"代码原文","reason":"判定理由"}],
"summary":"总体实现真实性结论"}
此核对表直接决定本维度得分:
- 真实 = 该项满分权重
- 部分实现 = 该项50%权重
- 占位(Mock) = 该项0分`;
function getDirectedTraceTemplate(): string {
return '\n' + DIRECTED_TRACE_TEMPLATE;
}
async function runReview(entryId: string, stage: 'A' | 'B', buildStatus?: 'done' | 'failed') {
activeCount++;
const tRun = Date.now();
@@ -1049,6 +1196,12 @@ async function executeReview(entryId: string) {
pipeLog(entryId, 'CLONE', cloneOk ? 'ok' : 'FAILED', Date.now() - tClone);
if (!cloneOk) return;
// 分支提醒:默认分支≠main 时写入日志(不阻断评审)
try {
const br = await simpleGit(dir).branchLocal();
if (br.current && br.current !== 'main') addLog(entryId, 'cloning', `⚠️ 默认分支为 ${br.current},不符合规范要求的 main`);
} catch { }
// L2考核:迟交超7个工作日 → 按0分处理,不启动评审(终态)
if (await maybeFinalizeL2Abandoned(entry, dir)) return;
@@ -1065,6 +1218,9 @@ async function executeReview(entryId: string) {
// L2考核:跨仓库查重初筛(确定性 flags,供人工复核,不自动定罪)
await runPlagiarismScreening(entry, dir);
// 评审真实性 P1:Map-Reduce 全量功能发现(供实现类维度对照验收基准)
const featureInventoryText = await discoverFeatureInventory(entryId, files);
// 方案②:项目理解文档(AI 解读代码生成,落库供 B 阶段与黑盒冒烟复用)
const tUnderstand = Date.now();
const understanding = await buildProjectUnderstanding(entryId, dir, files, codeStats);
@@ -1240,11 +1396,13 @@ ${overviewFileBlock}
const dimensions: any[] = [];
const toRun = [...standardDims];
const l2ExtraContexts = buildL2ExtraContexts(entry);
const auditExtra = buildAuditExtraContexts(entryId, standardDims, files, featureInventoryText);
const mergedExtra = Object.keys(auditExtra).length > 0 ? { ...l2ExtraContexts, ...auditExtra } : l2ExtraContexts;
const runNext = async () => {
while (toRun.length > 0) {
const dim = toRun.shift()!;
const tDim = Date.now();
const r = await runSubAgent(dim, projectContext, files, buildResult, startResult, browseResult, entry.category_tag, baseBranchDiff, agentGateReport, testEvidence, undefined, l2ExtraContexts);
const r = await runSubAgent(dim, projectContext, files, buildResult, startResult, browseResult, entry.category_tag, baseBranchDiff, agentGateReport, testEvidence, undefined, mergedExtra);
if (r) dimensions.push(r);
pipeLog(entryId, ' DIM', `${r?.name || '?'}${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`);
}
@@ -1273,7 +1431,7 @@ ${overviewFileBlock}
};
for (const d of dimensions) {
const v = classifyVerifiability(d, verifEvidence);
pipeLog(entryId, ' VERIF', `${d.name} raw=${d.score} tier=${v.tier} capped=${v.capped} eff=${v.effectiveScore}`);
pipeLog(entryId, ' VERIF', `${JSON.stringify(d.name)} raw=${d.score} tier=${v.tier} capped=${v.capped} eff=${v.effectiveScore} ev=${JSON.stringify(verifEvidence)}`);
if (v.capped && d.score > v.effectiveScore) {
d.score = v.effectiveScore;
(d as any).verifiability = v;
@@ -1284,6 +1442,30 @@ ${overviewFileBlock}
totalScore = dimensions.reduce((s, d) => s + d.score, 0);
}
// AI 日志确定性审计封顶(2026-08-26):占位率>50% 或 git一致率<30% → 封顶
let calibrationExplanation = '';
const aiLogFile = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(f.path));
if (aiLogFile && typeof aiLogFile.content === 'string') {
try {
const gitFiles = await collectGitChangedFiles(dir);
const la = auditAiUsageLog(aiLogFile.content, gitFiles);
pipeLog(entryId, 'LOGAUDIT', `lines=${la.lineCount} records=${la.recordCount} placeholder=${Math.round(la.placeholderRatio*100)}% gitConsist=${la.gitConsistencyRate}`);
const logDim = dimensions.find(d => d.name.includes('AI使用日志') || d.name.includes('AI协作过程记录'));
if (logDim) {
let cap: number | null = null;
let capNote = '';
if (la.lineCount < 5) { cap = Math.floor(logDim.maxScore * 0.2); capNote = '日志为空或过短'; }
else if (la.placeholderRatio > 0.5) { cap = Math.min(cap ?? Infinity, Math.floor(logDim.maxScore * 0.3)); capNote = '占位记录占比超50%'; }
if (la.gitConsistencyRate != null && la.gitConsistencyRate < 0.3 && la.consistencyChecked >= 5) { cap = Math.min(cap ?? Infinity, Math.floor(logDim.maxScore * 0.2)); capNote = '日志与git历史一致率低于30%'; }
if (cap != null && logDim.score > cap) {
logDim.score = cap;
calibrationExplanation += `\n\n日志审计封顶:\n- ${logDim.name}${cap}${capNote}`;
(logDim as any).auditNote = capNote;
}
}
} catch { }
}
// Phase 3b: AI calibration
addLog(entryId, 'analyzing', '正在校准评分...');
const calibrationPrompt = `你是一个评审校准Agent。以下各维度的评分和评语来自子Agent的独立评审。请检测跨维度语义矛盾(例如:开发范式说"无任何设计"但实现完整度却发现了3个Agent协作机制;效果数据满分但代码规模维度却显示几乎无实现)。
@@ -1300,7 +1482,6 @@ ${JSON.stringify(dimensions.map(d => ({ name: d.name, score: d.score, maxScore:
- 维度名必须与输入完全一致`;
const calibrationRaw = await callDeepSeek(calibrationPrompt, 2, 'calibrate');
let calibrationExplanation = '';
let contradictions: { name: string; direction: 'over' | 'under' }[] = [];
try {
if (calibrationRaw) {
@@ -1500,11 +1681,13 @@ ${overviewFileBlockA}
const dimensionsA: any[] = [];
const toRunA = [...aDims];
const l2ExtraContextsA = buildL2ExtraContexts(entry);
const auditExtraA = buildAuditExtraContexts(entryId, aDims, files, featureInventoryText);
const mergedExtraA = Object.keys(auditExtraA).length > 0 ? { ...l2ExtraContextsA, ...auditExtraA } : l2ExtraContextsA;
const runNextA = async () => {
while (toRunA.length > 0) {
const dim = toRunA.shift()!;
const tDim = Date.now();
const r = await runSubAgent(dim, projectContextA, files, EMPTY_BUILD_RESULT, undefined, undefined, entry.category_tag, baseBranchDiff, agentGateReport, undefined, undefined, l2ExtraContextsA);
const r = await runSubAgent(dim, projectContextA, files, EMPTY_BUILD_RESULT, undefined, undefined, entry.category_tag, baseBranchDiff, agentGateReport, undefined, undefined, mergedExtraA);
if (r) dimensionsA.push(r);
pipeLog(entryId, ' DIM', `${r?.name || '?'}${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`);
}
@@ -1750,11 +1933,13 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
const dimensionsB: any[] = [];
const toRunB = [...bDims];
const l2ExtraContextsB = buildL2ExtraContexts(entry);
const auditExtraB = buildAuditExtraContexts(entryId, bDims, files, '');
const mergedExtraB = Object.keys(auditExtraB).length > 0 ? { ...l2ExtraContextsB, ...auditExtraB } : l2ExtraContextsB;
const runNextB = async () => {
while (toRunB.length > 0) {
const dim = toRunB.shift()!;
const tDim = Date.now();
const r = await runSubAgent(dim, projectContextB, files, buildResult, startResult, browseResult, entry.category_tag, '', undefined, testEvidence, smokeEvidence, l2ExtraContextsB);
const r = await runSubAgent(dim, projectContextB, files, buildResult, startResult, browseResult, entry.category_tag, '', undefined, testEvidence, smokeEvidence, mergedExtraB);
if (r) dimensionsB.push(r);
pipeLog(entryId, ' DIM', `${r?.name || '?'}${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`);
}
@@ -1851,6 +2036,25 @@ ${JSON.stringify(dimensionsB.map(d => ({ name: d.name, score: d.score, maxScore:
// scoreB + 合???A+B
let scoreB = 0;
let maxScoreB = 0;
// AI 日志确定性审计封顶(B 阶段)
{
const aiLogDimB = [...aDimensions, ...dimensionsB].find(d => d.name.includes('AI使用日志') || d.name.includes('AI协作过程记录'));
const logFileB = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(path.basename(f.path)));
if (aiLogDimB && logFileB && typeof (logFileB as any).content === 'string') {
try {
const gitFiles = await collectGitChangedFiles(dir);
const la = auditAiUsageLog((logFileB as any).content, gitFiles);
pipeLog(entryId, 'LOGAUDIT_B', `placeholder=${Math.round(la.placeholderRatio*100)}% gitConsist=${la.gitConsistencyRate}`);
let cap: number | null = null;
if (!la.exists || la.lineCount < 5) { cap = Math.floor(aiLogDimB.maxScore * 0.2); }
else if (la.placeholderRatio > 0.5) { cap = Math.min(cap ?? Infinity, Math.floor(aiLogDimB.maxScore * 0.3)); }
if (la.gitConsistencyRate != null && la.gitConsistencyRate < 0.3) { cap = Math.min(cap ?? Infinity, Math.floor(aiLogDimB.maxScore * 0.2)); }
if (cap != null && aiLogDimB.score > cap) { aiLogDimB.score = cap; calibrationExplanationB += `\n\n日志审计封顶:\n- ${aiLogDimB.name}${cap}`; }
} catch { }
}
}
for (const d of dimensionsB) {
scoreB += Math.round(d.score);
maxScoreB += d.maxScore;
+9
View File
@@ -276,12 +276,21 @@ export function isPureGainDim(name: string): boolean {
return PURE_GAIN_KEYS.some(k => (name || '').includes(k));
}
// 定性设计维度豁免(2026-08-26):名称含"设计合理性/清晰度/合理性"等,
// 考察的是设计思路而非量化结果,不参与 C 档封顶(修复跨队伍环境因素不公平)。
const QUALITATIVE_DESIGN_KEYS = ['设计合理', '清晰度', '设计思路'];
export function isQualitativeDesignDim(name: string): boolean {
return QUALITATIVE_DESIGN_KEYS.some(k => (name || '').includes(k));
}
export function classifyVerifiability(
dim: { name: string; score: number; maxScore: number },
evidence: { hasBenchmarkEvidence?: boolean; hasEffectEvidence?: boolean; hasBuildEvidence?: boolean } = {}
) {
if (evidence.hasBenchmarkEvidence) return { tier: 'A' as const, capped: false, effectiveScore: dim.score, note: '有确定性基准证据(seed-defect benchmark' };
const strict = isPureGainDim(dim.name);
// 定性设计维度(如"提效设计合理性")不参与 C 档封顶——考察设计思路而非量化结果
if (isQualitativeDesignDim(dim.name)) return { tier: 'B' as const, capped: false, effectiveScore: dim.score, note: '' };
// 非效果维度直接 B;纯增益维度即使有测试通过证据也不豁免(严格基准制)
if (!isEffectDim(dim.name) || (evidence.hasEffectEvidence && !strict)) return { tier: 'B' as const, capped: false, effectiveScore: dim.score, note: '' };
const cap = Math.floor(dim.maxScore * 0.3);