初始提交:ai-review 项目当前版本(含赛道一/二提交规范修订与时间节点文档)

This commit is contained in:
hangshuo652
2026-08-23 11:52:45 +08:00
commit 4da7044c4b
152 changed files with 33490 additions and 0 deletions
@@ -0,0 +1,808 @@
# AI评审可信度改进 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 通过「多次评审聚合排名」+「提效幅度→可验证能力三档」+「校准证据性去LLM化」+「overall 中性边界统一」+「视频URL检测」+「维度级聚合报告」+「人机标定」,把评审从"单次LLM骰子"提升为"可复现、语义诚实、可逐步对标"的机制。
**Architecture:** 全部改动落在后端评审管线与排名展示层,不新增前端页面。排名聚合在 `projects.ts:buildSummary``entries.ts` 读取处注入纯函数 `aggregateScores`/`aggregateEntryScores`(基于 `review_snapshots.score` 列,**先校验标准一致**);提效幅度维度通过 `classifyVerifiability` 在**校准之前**判档并封顶(**效果证据判据,基准按 entry 落库 benchmark_json**);校准 L1 改为**证据性矛盾**确定性检测(效果维度永不 upshift);overall 合成输入加 `[中性证据]` 标记(区分环境失败与真缺测试);`detectDemoVideo` 扩展根目录 README 视频 URL(弱证据降权);详情/PDF 维度表用 `averageDimensions` 维度级聚合;`review_snapshots``score` 列 + backfill。
**Tech Stack:** Express + TypeScript + better-sqlite3(后端),React + Vite(前端仅展示聚合值)。测试用 vitest,全量须 `--no-file-parallelism`
---
## 决策记录(讨论结论,2026-08-19,含两轮对抗性审核修正)
1. **排名用多次评审聚合**(用户确认):entry 保留单次最新分,但排名/汇总用**最近 N 次(默认 3)final_score 的中位数**N=2 取平均、N=1 取该次)。聚合值单独展示,不改 `final_score` 字段语义。
- **修正(审核 A**:聚合分数**不从 ai_report 解析**(它只有 totalScore、丢迟交扣分),而是给 `review_snapshots``score REAL` 列,写快照时一并存 final_score,聚合读该列。历史快照需 **backfill**(从 ai_report.totalScore best-effort 回填)。
- **修正(审核 坑5)**:评审次数 <3 的 entry 标记为"初评(未达聚合样本)",排名区分"正式分(聚合)"与"初评分(单次)",避免"少评=占便宜"。
- **修正(审核 ①)**:聚合前校验各快照 `standard_snapshot` 一致;不一致则不聚合、退化为单次分。
2. **提效幅度 → 可验证能力三档**(专家复核修正):不删维度名(标准 MD 由管理员上传,改动会漂移),而是在**评分后加确定性"可验证能力判档"**
- A 档:有可运行基准证据(决赛圈跑 seed-defect benchmark)→ 客观数字
- B 档:无基准但**有自报效果数据或确定性测试证据** → 按现有证据正常打分
- C 档:数据缺位 → 分数封顶低档(如 0-3),**标注"数据缺位(未证明),非无效"**,不得用 AI 自评代替
- **修正(审核 B)**:判据**不得用 `hasBuildEvidence`(构建成功与"效果可验证"无关)**。B 档 = 有效果证据(自报数据/测试通过/覆盖率);只有构建没有效果数据 → 仍进 C 档。
- **修正(审核 坑1)**:基准证据**按 entry 落库**`entries.benchmark_json`),不用进程级 env 变量(MAX_CONCURRENT=3 并发会串数据)。
- **修正(审核 坑3)**:**判档先于校准执行**;效果维度只允许被确定性 L1 **下调(over**,永不 upshift——效果维度的诚实由三档封顶独揽,校准不碰它。
- **修正(审核 ④)**:C 档 note 必须渲染到前端详情 + PDF,不能只进 ai_report。
3. **校准 L1 去 LLM 化**`computeCalibration` 新增确定性**证据性矛盾**检测,LLM contradictions 仅作补充、不再单独承担 L1。
- **修正(审核 C)**:确定性 L1 **只触发证据性矛盾**(基准检出缺陷但 AI 给 0、测试通过有覆盖率但效果给低分),**禁止**用"实现高分+效果无数据"当 under 理由(那是 C 档的职责,L1 一抬就与三档封顶打架)。
4. **overall 中性统一**`synthesizeOverall` 的 evidenceLines 中,中性证据(测试环境失败等)加 `[中性证据]` 标记;prompt 明确"中性证据不得降级为弱点"。
- **修正(审核 ②)**:测试"0/0"按 tryTest summary 措辞区分:明确报"测试运行失败(环境)"→ 中性;"无测试文件/未发现测试"→ 真实弱点,不得误标中性。
5. **视频 URL 检测**`detectDemoVideo` 除扫描视频文件外,扫描**根目录 README 及根级 docs/*.md** 中 bilibili/youtube 等视频链接。
- **修正(审核 ③)**:URL 链接是**弱证据**,判定结果带 source 区分(本地文件=强,外部链接=弱),评语注明"外部链接,未核验内容"。只扫根目录,不扫全部 *.md(CLAUDE.md 竞品链接会误报)。
6. **基准测试只对决赛圈作品跑**(成本控制),初筛用现有证据链。seed 库赛前临时生成、不公开(Goodhart 为已知上限)。检出率与基线分开呈现,不做差值当分数。
7. **Skill 测评**(后续阶段):静态只做依赖完整性门禁,不做质量判定;动态变体测试留待有真实 skill 参赛作品时再建。
8. **报告维度级聚合(审核 坑4**:详情页/PDF 的维度表改为跨快照**维度级聚合**(复用已存在的 `averageDimensions`review.service.ts:2354),展示"聚合维度分 + 各次分差"。总分与维度拆解都稳定,评审报告才"可参考"。
9. **人机标定(审核 ⑤,Task 10)**:方案外但决定"绝对准确"——用 2-3 个人工专家打分对照样本作品,得出 AI 分数偏差基线,写进文档作可信度天花板。
**明确不做(YAGNI**:不新建前端页面;不动 `final_score` 字段语义;不引入外部基准数据集(成本高);不对标准 MD 做维度改名。
---
## Task 1: 快照分数列 + 新增纯函数 `aggregateScores` + 单测
**Files:**
- Modify: `server/src/db.ts``review_snapshots``score REAL` 列 + backfill
- Modify: `server/src/services/standard-utils.ts`(追加导出函数)
- Test: `server/src/__tests__/standard-utils.test.ts`
**Step 1: DB migration + backfill**
`db.ts` 追加 migration
```ts
try { db.exec("ALTER TABLE review_snapshots ADD COLUMN score REAL"); } catch (e) {}
// backfill:历史快照无 score,从 ai_report.totalScore 解析(best-effort,仅一次性)
try {
db.exec(`UPDATE review_snapshots SET score = (SELECT json_extract(ai_report, '$.totalScore'))
WHERE score IS NULL AND ai_report IS NOT NULL`);
} catch (e) {}
```
> 注意:后续写入快照(`review.service.ts` 两处 `INSERT INTO review_snapshots`)必须同时存 `score = finalScore`(已含迟交扣分),不再依赖解析 ai_report。
**Step 2: 写失败测试**
`server/src/__tests__/standard-utils.test.ts` 追加:
```ts
import { aggregateScores } from '../services/standard-utils';
describe('aggregateScores', () => {
it('空数组返回 null', () => {
expect(aggregateScores([])).toBeNull();
});
it('1 次返回该次值', () => {
expect(aggregateScores([78])).toEqual({ value: 78, count: 1, method: 'single' });
});
it('2 次取平均', () => {
expect(aggregateScores([78, 69])).toEqual({ value: 74, count: 2, method: 'avg' });
});
it('3 次取中位数(去抖)', () => {
expect(aggregateScores([73, 78, 69])).toEqual({ value: 73, count: 3, method: 'median' });
});
it('4 次取中位数', () => {
expect(aggregateScores([73, 78, 69, 90])).toEqual({ value: 76, count: 4, method: 'median' }); // 排序后[69,73,78,90]中位=(73+78)/2=75.5→round 76
});
});
```
**Step 3: 运行确认失败**
Run: `npx vitest run src/__tests__/standard-utils.test.ts --no-file-parallelism`
Expected: FAIL with "aggregateScores is not a function"
**Step 4: 实现**
`standard-utils.ts` 末尾追加:
```ts
/**
* 多次评审聚合(2026-08-19):排名用稳健统计,防单次 LLM 抖动与刷高。
* - 空 → null;1 次 → 该次值;2 次 → 平均(round);≥3 次 → 中位数(偶数取中间两值平均,round)
*/
export function aggregateScores(scores: number[]): { value: number; count: number; method: 'single' | 'avg' | 'median' } | null {
const valid = (scores || []).filter(n => typeof n === 'number' && isFinite(n));
if (valid.length === 0) return null;
if (valid.length === 1) return { value: Math.round(valid[0]), count: 1, method: 'single' };
if (valid.length === 2) return { value: Math.round((valid[0] + valid[1]) / 2), count: 2, method: 'avg' };
const sorted = [...valid].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 === 1
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
return { value: Math.round(median), count: valid.length, method: 'median' };
}
/**
* 从 review_snapshots 行提取最近 N 次 score 并聚合。
* 要求:全部快照 standard_snapshot 一致,否则返回 null(分数不可比)。
*/
export function aggregateEntryScores(
snapshotRows: { score: number | null; standard_snapshot: string | null }[],
recentN = 3
): { value: number; count: number; method: 'single' | 'avg' | 'median' } | null {
const rows = (snapshotRows || []).filter(r => typeof r.score === 'number' && isFinite(r.score as number));
if (rows.length === 0) return null;
const stds = new Set((snapshotRows || []).filter(r => r.standard_snapshot).map(r => r.standard_snapshot));
if (stds.size > 1) return null; // 标准不一致 → 分数不可比,不聚合
const scores = rows.slice(-recentN).map(r => r.score as number);
return aggregateScores(scores);
}
```
**Step 5: 运行确认通过**
Run: `npx vitest run src/__tests__/standard-utils.test.ts --no-file-parallelism`
Expected: PASS
**Step 6: Commit**
```bash
git add server/src/db.ts server/src/services/standard-utils.ts server/src/__tests__/standard-utils.test.ts
git commit -m "feat(review): snapshot score column + aggregateScores for multi-review ranking"
```
---
## Task 2: 排名/汇总/条目列表接入聚合值
**Files:**
- Modify: `server/src/routes/projects.ts:19-54`buildSummary 排名用聚合)
- Modify: `server/src/routes/entries.ts:54-71`(列表 items 附加 aggregate_score
- Test: `server/src/__tests__/api.test.ts` 或新建 `server/src/__tests__/aggregate-api.test.ts`
**背景**`review_snapshots` 新增 `score` 列(Task 1),聚合读它,不再解析 ai_report。
**Step 1: 写失败测试**
新建 `server/src/__tests__/aggregate-api.test.ts`
```ts
import { describe, expect, it } from 'vitest';
import { aggregateEntryScores } from '../services/standard-utils';
describe('aggregateEntryScores', () => {
const base = { score: 78, standard_snapshot: 'std-A' };
it('标准一致时聚合最近 3 次 score', () => {
const rows = [
{ ...base, score: 73 },
{ ...base, score: 78 },
{ ...base, score: 69 },
];
const r = aggregateEntryScores(rows, 3);
expect(r).toEqual({ value: 73, count: 3, method: 'median' });
});
it('标准不一致 → 返回 null(不可比,不聚合)', () => {
const rows = [
{ score: 73, standard_snapshot: 'std-A' },
{ score: 78, standard_snapshot: 'std-B' },
];
expect(aggregateEntryScores(rows, 3)).toBeNull();
});
it('空/全空 score → null', () => {
expect(aggregateEntryScores([])).toBeNull();
expect(aggregateEntryScores([{ score: null, standard_snapshot: 'std-A' }])).toBeNull();
});
it('score 为 null 的行跳过', () => {
const rows = [{ score: null, standard_snapshot: 'std-A' }, { score: 80, standard_snapshot: 'std-A' }];
const r = aggregateEntryScores(rows, 3);
expect(r).toEqual({ value: 80, count: 1, method: 'single' });
});
});
```
**Step 2: 运行确认失败**
Run: `npx vitest run src/__tests__/aggregate-api.test.ts --no-file-parallelism`
Expected: FAIL
**Step 3: 实现(已并入 Task 1 Step 4**
`aggregateEntryScores` 已在 standard-utils.ts 实现。此任务仅接线。
**Step 4: 接入路由**
`projects.ts:20``buildSummary`:为避免 N+1 查询(50 entry × 3 快照 = 150 次往返),用**单条 GROUP BY 查询**取每 entry 最近 3 次 score
```ts
// 一次性取该 project 下所有 entry 的最近 3 条快照 score(标准一致才聚合)
const aggRows = db.prepare(`
SELECT s.entry_id, s.score, s.standard_snapshot
FROM review_snapshots s
WHERE s.entry_id IN (SELECT id FROM entries WHERE project_id = ? AND status IN ('review_done','admin_reviewed'))
ORDER BY s.entry_id, s.attempt ASC
`).all(projectId) as any[];
const aggByEntry: Record<string, any> = {};
for (const row of aggRows) {
if (!aggByEntry[row.entry_id]) aggByEntry[row.entry_id] = [];
aggByEntry[row.entry_id].push(row);
}
```
对每个 entry
```ts
const agg = aggregateEntryScores(aggByEntry[e.id] || [], 3);
const isFormal = agg && agg.count >= 3; // 正式分(聚合);<3 为初评
const displayScore = isFormal ? agg.value : (e.final_score || e.raw_score);
```
- `byCategory` entries 加 `score: displayScore``aggregate_count: agg?.count ?? 0``is_formal: isFormal`**<3 次标"初评(未达聚合样本)"**),`rank` 按 displayScore 重排
- `byParticipant` 同理
`entries.ts` GET `/` 列表:同样用一条 GROUP BY 查询附加 `aggregate_score` / `aggregate_count` / `is_formal`(不逐 entry 查)。
**Step 5: 运行确认通过**
Run: `npx vitest run src/__tests__/aggregate-api.test.ts --no-file-parallelism`
Expected: PASS
Run(全量): `npx vitest run --no-file-parallelism` Expected: 全绿
**Step 6: Commit**
```bash
git add server/src/routes/projects.ts server/src/routes/entries.ts server/src/__tests__/aggregate-api.test.ts
git commit -m "feat(review): rank by aggregated multi-review score"
```
---
## Task 3: 可验证能力判档(三档,C 档封顶)
**Files:**
- Modify: `server/src/db.ts``entries``benchmark_json TEXT`,按 entry 存基准证据)
- Modify: `server/src/services/standard-utils.ts`(新增 `classifyVerifiability` + 单测)
- Modify: `server/src/services/review.service.ts`(评审后处理:**校准之前**判档)
- Test: `server/src/__tests__/standard-utils.test.ts`
**Step 1: 写失败测试**
```ts
describe('classifyVerifiability', () => {
const dim = { name: '效果评估与数据', score: 10, maxScore: 10 };
it('有确定性基准证据 → A 档,不封顶', () => {
const r = classifyVerifiability(dim, { hasBenchmarkEvidence: true });
expect(r.tier).toBe('A');
expect(r.capped).toBe(false);
});
it('无基准但有效果数据(自报数据/测试通过/覆盖率) → B 档,不封顶', () => {
const r = classifyVerifiability(dim, { hasEffectEvidence: true });
expect(r.tier).toBe('B');
expect(r.capped).toBe(false);
});
it('有构建证据但无效果数据 → 仍 C 档(构建成功≠效果可验证)', () => {
const r = classifyVerifiability(dim, { hasBuildEvidence: true });
expect(r.tier).toBe('C');
expect(r.capped).toBe(true);
});
it('无证据且维度属效果/提效类 → C 档,分数封顶 maxScore*0.3 向下取整', () => {
const r = classifyVerifiability(dim, {});
expect(r.tier).toBe('C');
expect(r.capped).toBe(true);
expect(r.effectiveScore).toBeLessThanOrEqual(3);
expect(r.note).toContain('数据缺位');
});
it('非效果/提效类维度即使无证据也不封顶', () => {
const d2 = { name: '代码规范', score: 4, maxScore: 5 };
const r = classifyVerifiability(d2, {});
expect(r.tier).toBe('B');
expect(r.capped).toBe(false);
});
});
```
**Step 2: 运行确认失败**
Run: `npx vitest run src/__tests__/standard-utils.test.ts --no-file-parallelism`
Expected: FAIL(函数不存在)
**Step 3: 实现**
`standard-utils.ts` 追加:
```ts
const EFFECT_EVIDENCE_KEYS = ['效果', '数据', '提效', '效率', '量化'];
export function isEffectDim(name: string): boolean {
return EFFECT_EVIDENCE_KEYS.some(k => name.includes(k));
}
export function classifyVerifiability(
dim: { name: string; score: number; maxScore: number },
evidence: { hasBenchmarkEvidence?: boolean; hasEffectEvidence?: boolean } = {}
) {
if (evidence.hasBenchmarkEvidence) return { tier: 'A' as const, capped: false, effectiveScore: dim.score, note: '有确定性基准证据(seed-defect benchmark' };
if (!isEffectDim(dim.name) || evidence.hasEffectEvidence) return { tier: 'B' as const, capped: false, effectiveScore: dim.score, note: '' };
const cap = Math.floor(dim.maxScore * 0.3);
return { tier: 'C' as const, capped: true, effectiveScore: Math.min(dim.score, cap), note: `数据缺位(未证明),非无效;C档封顶 ${cap}/${dim.maxScore}` };
}
```
**Step 4: DB migration(基准证据按 entry 落库,非 env 变量)**
`db.ts` 追加:
```ts
try { db.exec("ALTER TABLE entries ADD COLUMN benchmark_json TEXT DEFAULT ''"); } catch (e) {}
```
**Step 5: 接入评审管线(判档先于校准)**
`review.service.ts` **`computeCalibration` 之前**、子 Agent 维度收敛之后(两处:单阶段 ~1120 附近、B 阶段 ~1560 附近)对 dimensions 判档:
```ts
const entryRow = db.prepare('SELECT benchmark_json FROM entries WHERE id = ?').get(entryId) as any;
const bench = entryRow?.benchmark_json ? (() => { try { return JSON.parse(entryRow.benchmark_json); } catch { return null; } })() : null;
const evidence = {
hasBenchmarkEvidence: !!(bench && bench.status === 'done'),
hasEffectEvidence: (testResult?.testsPassed > 0) || (testResult?.coverage != null) || (entry.self_reported_effect !== ''), // 自报数据字段(若标准里有则从 ai_report 或单独列取)
};
for (const d of dimensions) {
const v = classifyVerifiability(d, evidence);
if (v.capped && d.score > v.effectiveScore) {
d.score = v.effectiveScore;
d.verifiability = v; // 附加到维度对象,写进 ai_report.dimensions
}
}
```
> 判档在**校准之前**(审核坑3):效果维度先被 C 档封顶,校准只能看到诚实的分数;且 Task 4 保证效果维度只降不升,二者不再互相抵消。
**Step 6: C 档 note 渲染(审核④)**
- 前端:条目详情维度表 + PDF 维度表,`d.verifiability?.note` 存在时显示灰色说明行
- 不只在 ai_report 里(用户看不到的说明等于没有)
**Step 7: 运行确认通过**
Run: `npx vitest run src/__tests__/standard-utils.test.ts --no-file-parallelism`
Expected: PASS
Run(全量): 全绿
**Step 8: Commit**
```bash
git commit -am "feat(review): tier-3 verifiability gating for effect/evidence dims (before calibration)"
```
---
## Task 4: 校准 L1 确定性结构矛盾检测
**Files:**
- Modify: `server/src/services/standard-utils.ts`(新增 `detectStructuralContradictions` + 单测)
- Modify: `server/src/services/review.service.ts`(把确定性矛盾并入 computeCalibration 输入)
**Step 1: 写失败测试**
```ts
describe('detectStructuralContradictions', () => {
it('测试通过有覆盖率但效果维度给 0 → 标记 under(证据性矛盾)', () => {
const dims = [
{ name: '效果评估与数据', score: 0, maxScore: 10 },
{ name: '实现完整度', score: 15, maxScore: 15 },
];
const r = detectStructuralContradictions(dims, { testPassed: true, hasCoverage: true });
expect(r.some(c => c.name.includes('效果') && c.direction === 'under')).toBe(true);
});
it('实现高分但效果无任何证据 → 不标记 under(缺数据归因归 C 档,L1 不得上抬)', () => {
const dims = [
{ name: '效果评估与数据', score: 0, maxScore: 10 },
{ name: '实现完整度', score: 15, maxScore: 15 },
];
const r = detectStructuralContradictions(dims, {});
expect(r).toEqual([]);
});
it('效果维度 0 但测试也失败/无覆盖率 → 不是证据性矛盾', () => {
const dims = [
{ name: '效果评估与数据', score: 0, maxScore: 10 },
{ name: '实现完整度', score: 15, maxScore: 15 },
];
const r = detectStructuralContradictions(dims, { testPassed: false });
expect(r).toEqual([]);
});
it('全维度均衡 → 无矛盾', () => {
const dims = [
{ name: '效果评估与数据', score: 6, maxScore: 10 },
{ name: '实现完整度', score: 9, maxScore: 15 },
];
expect(detectStructuralContradictions(dims, { testPassed: true })).toEqual([]);
});
it('最高最低分差 > 阈值但都非效果类 → 不误报', () => {
const dims = [
{ name: '代码规范', score: 5, maxScore: 5 },
{ name: '演示与文档', score: 0, maxScore: 5 },
];
expect(detectStructuralContradictions(dims, {})).toEqual([]);
});
});
```
**Step 2: 运行确认失败**
Expected: FAIL
**Step 3: 实现**
```ts
export interface CalibrationContradiction { name: string; direction: 'over' | 'under'; reason: string }
export interface StructuralEvidence { testPassed?: boolean; hasCoverage?: boolean; benchmarkDetectedCount?: number; benchmarkTotal?: number; }
export function detectStructuralContradictions(
dims: { name: string; score: number; maxScore: number }[],
evidence: StructuralEvidence = {}
): CalibrationContradiction[] {
const out: CalibrationContradiction[] = [];
const effect = dims.filter(d => d.maxScore > 0 && isEffectDim(d.name));
if (effect.length === 0) return out;
// 仅"证据性矛盾"才触发 L1。缺数据归因归 C 档(classifyVerifiability),L1 不得据此上抬效果维度。
const positiveEvidence = evidence.testPassed || evidence.hasCoverage
|| ((evidence.benchmarkTotal ?? 0) > 0 && (evidence.benchmarkDetectedCount ?? 0) > 0);
if (positiveEvidence) {
for (const d of effect) {
const ratio = d.score / d.maxScore;
if (ratio <= 0.05) {
out.push({ name: d.name, direction: 'under', reason: '确定性规则:存在真实测试/基准证据但效果维度接近 0,疑似低估' });
}
}
}
// 反向(效果维度只降不升,但 over 允许):效果类高分而实现类全低 → 效果可能被高估
const impl = dims.filter(d => d.maxScore > 0 && !isEffectDim(d.name));
if (impl.length > 0) {
const effectRatioMin = Math.min(...effect.map(d => d.score / d.maxScore));
const implRatioAvg = impl.reduce((s, d) => s + d.score / d.maxScore, 0) / impl.length;
if (effectRatioMin >= 0.8 && implRatioAvg <= 0.3) {
for (const d of effect) {
out.push({ name: d.name, direction: 'over', reason: '确定性规则:效果类高而实现类低,疑似高估' });
}
}
}
return out.slice(0, 3);
}
```
**Step 4: 接入 computeCalibration 调用处**
`review.service.ts` 三处(~1145 / ~1353 / ~1601)把 LLM contradictions 与确定性矛盾合并,并传证据:
```ts
const deterministicC = detectStructuralContradictions(dimensions, {
testPassed: (testResult?.testsPassed ?? 0) > 0,
hasCoverage: testResult?.coverage != null,
benchmarkDetectedCount: bench?.detectedCount,
benchmarkTotal: bench?.total,
});
const contradictions = [...deterministicC, ...(llmContradictions || [])];
```
(保留 LLM 矛盾作为补充,但 L1 不再单独依赖 LLM。)
> **硬规则(审核C**`computeCalibration` 处理 contradictions 时,**效果维度的 `under` 一律丢弃**`direction === 'under' && isEffectDim(name)` → 跳过),效果维度诚实由三档封顶负责,校准永不 upshift 效果维度。
**Step 5: 运行确认通过**
Run: 单测 + 全量 Expected: 全绿
**Step 6: Commit**
```bash
git commit -am "feat(review): deterministic L1 evidence-based contradictions (effect dims never upshift)"
```
---
## Task 5: overall 中性边界统一
**Files:**
- Modify: `server/src/services/review.service.ts`synthesizeOverall 的 evidenceLines 生成处加 [中性证据] 标记)
- Modify: `server/src/services/review.service.ts`synthesizeOverall prompt 加中性约束)
- Test: `server/src/__tests__/overall.test.ts`
**Step 1: 定位 evidenceLines 组装**
单阶段(~1210 附近)与 B 阶段(~1680 附近)里 `evidenceLines` 数组 push 测试/冒烟/浏览结果处。找到形如:
```ts
evidenceLines.push(`测试: ${...}`);
```
的若干行。
**Step 2: 加中性标记**
对**环境失败/未执行**类证据(tryTest 环境失败、BROWSE 未启动、冒烟 skipped)在字符串前缀加 `[中性证据] `。实现一个 helper
```ts
function neutralEvidence(line: string): string { return `[中性证据] ${line}`; }
```
**测试"0/0"归类(审核②)**——按 tryTest summary 措辞区分,不要全标中性:
```ts
// 明确环境失败 → 中性
if (testResult?.envFailure) evidenceLines.push(neutralEvidence(`测试: ${testResult.summary}`));
// "无测试文件/未发现测试" → 真实弱点,不标中性
else if (testResult?.noTestsFound) evidenceLines.push(`测试: ${testResult.summary}`);
```
> 判定依据:`test-runner.ts` 输出的 summary 含"测试运行失败(中性,不因此扣分)"为环境失败;含"未发现测试/no tests"为真实缺测试。若 summary 无法区分则按 envFailure 标志位判断,不猜。
**Step 3: prompt 加约束**
在 synthesizeOverall prompt 的约束块加:
```
- 标有 [中性证据] 的内容是系统环境限制(非作品问题),不得列为不足或负面评价
```
**Step 4: 测试**
`overall.test.ts` 加一条纯函数测试(把中性判定抽成纯函数 `isNeutralEvidence(line)` 或直接在 prompt 字符串断言):
```ts
it('overall prompt 禁止将中性证据降级为弱点', () => {
const p = buildOverallPromptForTest({ evidenceLines: ['[中性证据] 测试环境失败(不因此扣分)'] });
expect(p).toContain('[中性证据]');
expect(p).toContain('不得列为不足');
});
```
(若抽纯函数则测纯函数。)
**Step 5: 运行确认通过**
单测 + 全量 Expected: 全绿
**Step 6: Commit**
```bash
git commit -am "fix(review): keep neutral evidence out of overall weaknesses"
```
---
## Task 6: 视频 URL 检测扩展
**Files:**
- Modify: `server/src/services/review.service.ts`detectDemoVideo 或其调用处)
- Modify: `server/src/__tests__/ide-video.test.ts`
**Step 1: 写失败测试**
`ide-video.test.ts` 追加:
```ts
describe('detectDemoVideo (URL 检测)', () => {
it('README 含 bilibili 链接 → 判定有演示视频(外部链接)', async () => {
const dir = makeTempWithFiles({ 'README.md': '## 演示\nbilibili: https://www.bilibili.com/video/BV1xx411c7mD' });
const r = await detectDemoVideo(dir);
expect(r.exists).toBe(true);
expect(r.source).toMatch(/url|link/i);
cleanup(dir);
});
it('README 含 youtube 链接 → 判定有', async () => {
const dir = makeTempWithFiles({ 'README.md': 'watch: https://youtu.be/dQw4w9WgXcQ' });
const r = await detectDemoVideo(dir);
expect(r.exists).toBe(true);
cleanup(dir);
});
it('无视频文件且无链接 → 判定无', async () => {
const dir = makeTempWithFiles({ 'README.md': 'no video here' });
const r = await detectDemoVideo(dir);
expect(r.exists).toBe(false);
cleanup(dir);
});
});
```
`makeTempWithFiles`/`cleanup` 沿用该文件既有 helper;若原文件无 helper 则改用 `fs.mkdtempSync`。)
**Step 2: 运行确认失败**
Expected: FAIL
**Step 3: 实现**
`detectDemoVideo` 增加根目录 README 扫描(**只扫根目录 README 与根级 docs/*.md**,避免 CLAUDE.md 竞品链接误报):
```ts
const VIDEO_URL_RE = /(bilibili\.com\/video|youtube\.com\/watch|youtu\.be|v\.qq\.com|douyin\.com\/video)/i;
// 在文件扫描之外,读根目录 README.md / README / 根级 docs/*.md 文件内容,匹配 VIDEO_URL_RE
```
**弱证据降权(审核③)**:返回值区分来源,prompt 注明:
```ts
// exists=true, source='file:<path>'(本地视频文件=强证据)或 source='url:<link>'(外部链接=弱证据)
// 注入 runSubAgent 时:弱证据行加"(外部链接,未核验内容)"
```
**Step 4: 运行确认通过**
单测 Expected: PASS
**Step 5: Commit**
```bash
git commit -am "feat(review): detect demo video via README links"
```
---
## Task 7: 前端展示聚合分 + 维度级聚合(排名/列表/详情/PDF)
**Files:**
- Modify: `web/src/components/ProjectView.tsx:479-483`(条目列表分数列显示聚合值并标注)
- Modify: `web/src/components/ProjectView.tsx`(汇总排名段,~1166-1190,用 aggregate_score 排序/显示)
- Modify: `server/src/routes/entries.ts:73-88`(详情接口:返回维度级聚合 + 各次分差)
- Modify: `server/src/services/pdf.service.ts`PDF 维度表用聚合维度分)
**Step 1: 后端已返回字段**
列表 items 已带 `aggregate_score` / `aggregate_count` / `is_formal`Task 2)。
**Step 2: 列表分数列**
`ProjectView.tsx` 分数单元格:
```tsx
{e.aggregate_count > 0 && e.is_formal
? `${e.aggregate_score}(聚合${e.aggregate_count}次)`
: e.aggregate_count > 0
? `${e.aggregate_score}(初评${e.aggregate_count}次)`
: (e.final_score ?? e.raw_score ?? '-')}
```
**Step 3: 详情接口返回维度级聚合(审核坑4)**
`entries.ts` GET `/:entryId`~73-88):已有 `snapshots`(全量)。追加计算:
```ts
import { averageDimensions } from '../services/review.service';
import { parseDimensions } from '../services/standards';
// 从 snapshots 的 ai_report 提取每次的 dimensions,用 averageDimensions 聚合
// 返回 dimensionsAgg: { dims: 聚合维度数组, perRun: [{attempt, score, dims}] }
```
> `averageDimensions(a, b, standard)` 只支持两两合并 → 多次评审做 reduce`runs.reduce((acc, run) => averageDimensions(acc, run.dims, standard))`。
> **注意**:若各次快照 `standard_snapshot` 不一致,维度名不同 → 不聚合,返回各次原始 dims。
详情页维度表显示:聚合维度分 + 每维"各次分差"(如 `19/20(历次 19,16,19`)。
**Step 4: PDF 维度表用聚合维度分**
`pdf.service.ts` 维度表行渲染处,改用详情接口传入的聚合维度;C 档 note(Task 3 Step 6)同步显示。
**Step 5: 前端类型与编译**
在类型声明(若存在 `EntryRow` interface)补 `aggregate_score?: number | null``aggregate_count?: number``is_formal?: boolean``dimensionsAgg?: any`
Run: `npx tsc --noEmit` Expected: exit 0
**Step 6: Commit**
```bash
git commit -am "feat(web): aggregated score + dimension-level aggregation in list/summary/detail/PDF"
```
---
## Task 8: 决赛圈基准框架(design-only + 脚手架,TDD
> 此任务为**加分项**,落地依赖真实决赛圈。seed 库赛前生成、不公开。
**Files:**
- Create: `server/src/services/benchmark.ts`(框架:`runBenchmark(dir, seedManifest)` 骨架 + 类型)
- Create: `server/src/__tests__/benchmark.test.ts`(框架单测)
- Modify: `server/src/routes/entries.ts`(决赛圈基准结果写 `entries.benchmark_json`
- Modify: `server/src/services/review.service.ts`(判档读 `entries.benchmark_json`Task 3 Step 5 已留口)
> **坑1 修复**:基准证据**按 entry 落库**`entries.benchmark_json`),不用 env 变量。决赛圈跑完基准后,通过管理端/脚本写入该 entry 的 benchmark_json`{ status: 'done', detectedCount, total, baselineDetectedCount, language }`),评审时判档读它。
**Step 1: 类型与骨架(TDD**
```ts
export interface SeedCase { id: string; file: string; defectType: 'syntax'|'logic'|'concurrency'|'security'|'performance'; lineHint?: number; }
export interface BenchmarkReport { detected: string[]; falsePositives: string[]; detectedCount: number; total: number; baselineDetectedCount: number; language: string; }
export async function runBenchmark(projectDir: string, seeds: SeedCase[], detect: (file: string) => Promise<string[]>): Promise<BenchmarkReport> { /* 骨架:循环 seed,调 detect,比对命中 */ }
```
**Step 2: 单测(mock detect**
- 注入 5 seeds + mock detect 返回其中 3 个 → 断言 detectedCount=3、total=5、baseline=0(未提供)
- 坏输入(空 seeds)→ 返回 total 0 报告,不抛异常
**Step 3: 编译 + 全量测试**
`npx tsc` + 全量 vitest Expected: 全绿
**Step 4: 文档化 seed 库规范**
`docs/design/05-评审流程修正方案.md` 追加 §2.11:seed 结构、注入方式(真实 bug 模式)、语言覆盖(JS/CSS/Java/SQL)、双人标注误报语料、行号容差、检出率与基线分开呈现。**不公开 seed 内容。**
**Step 5: Commit**
```bash
git commit -am "feat(review): benchmark framework scaffold + design doc"
```
---
## Task 9: 文档与 AGENTS.md 同步
**Files:**
- Modify: `docs/design/05-评审流程修正方案.md`(聚合排名、可验证能力三档、中性统一、URL检测、维度级聚合)
- Modify: `AGENTS.md`aggregateScores / aggregateEntryScores / classifyVerifiability / neutralEvidence / detectDemoVideo URL / benchmark_json 列)
- Modify: `web/e2e` 相关断言若受影响(聚合分数列文案)
**Step 1: 写设计书**
§2.11 可验证能力三档 + 聚合排名 + 维度级聚合;更新 AGENTS.md 关键事实表与核心流程。
**Step 2: 全量回归**
Run: `npx vitest run --no-file-parallelism` Expected: 全绿
Run: `cd server && npx tsc``cd web && npx tsc --noEmit` Expected: 全绿
**Step 3: Commit**
```bash
git commit -am "docs: sync aggregation/verifiability/neutral design into 05 and AGENTS.md"
```
---
## Task 10: 人机标定(可信度天花板,审核⑤)
> 决定"绝对准确"的一步:多次聚合解决统计稳健、三档解决语义诚实,但"AI 给 78 到底准不准"只有人工专家对照才能回答。
**Files:**
- Create: `docs/design/06-人机标定方案.md`(流程 + 结果表)
- Modify: `docs/plans/2026-08-19-评审可信度改进.md` 已知边界
**Step 1: 选样本**
从已评审作品选 2-3 个代表(高/中/低分各一,如净码特攻 73、逸飞冲天 63、任一低分作)。
**Step 2: 人工打分**
1-2 位专家按同一标准 MD 对样本作品独立打分(只看证据包,不看 AI 分数,避免锚定)。
**Step 3: 对比分析**
- 计算 AI 分 vs 人工分的**偏差**(每维度 + 总分)
- 得出偏差基线,写进 06 文档:如"AI 总分偏高 +5,效果类维度系统性偏高 +8"——后续可做**固定偏差修正**或至少让排名用相对序(rank 一致性而非绝对分)
- 输出:`AI 分 / 人工分 / 偏差 / 每维度偏差`
**Step 4: 决策**
- 偏差小 → 维持现状,只写记录
- 偏差系统性 → 加"修正系数"或在文档声明"分数需 +X 解读",**不自动改分**(避免双重修正)
**Step 5: Commit**
```bash
git commit -am "docs: human calibration baseline (AI vs expert score deviation)"
```
---
## 验证路径(最终)
1. `cd server && npx tsc`exit 0
2. `cd server && npx vitest run --no-file-parallelism`(全绿,含新增 aggregate/verifiability/contradiction/URL 单测)
3. `cd web && npx tsc --noEmit`exit 0
4. **backfill 验证(坑2**`npx tsc` 后跑一次 migration,确认历史快照 score 已回填(`SELECT score, ai_report FROM review_snapshots WHERE score IS NULL` 返回 0 行)
5. 生产库重启后:对净码特攻再做 1 次评审(现有 snapshots 已有 3 次:69/73/78),确认 summary 排名显示**聚合分(中位数 73,聚合3次)**而详情仍保留单次最新分;且 4 次后中位数更新(69,73,78,新)
6. 手工:前端刷新,列表/汇总显示"73(聚合3次)";整体评价段中性证据不再出现在不足;C 档 note 出现在维度表
7. 校准逻辑验证:构造"测试通过+覆盖率高但效果维度 0"的合成用例,确认 L1 触发 under;再构造"无任何效果证据"用例,确认不触发(L1 不上抬缺数据)
## 已知边界(写进文档)
- Goodhart:固定 seed 库会被调优 → 赛前生成、不公开、每届更换,接受为上限
- 聚合只对**标准一致**且 ≥2 次的 entry 生效;标准不一致退化为单次分
- **<3 次评审的 entry 标记"初评(未达聚合样本)"**,排名区分正式分/初评分,避免"少评占便宜"
- C 档封顶会改变分数语义 → 评审说明 + 前端维度表 + PDF 报告显示 note,避免"莫名低分"
- 历史快照 backfill 为 best-effort(旧 ai_report 缺失 totalScore 时留 NULL,该 entry 退化为单次分)
- 效果维度的诚实由三档封顶独揽,校准永不 upshift 效果维度(确定性 L1 仅证据性触发)
- 不引入外部基准数据集;真实基准仅决赛圈,按 entry 落库(benchmark_json),非 env 变量
- 绝对准确性未验证 → Task 10 人机标定得出偏差基线前,分数用于排名(相对序)可信,绝对解读需 +X