评审真实性增强:前端硬规则+人工启动方案+文档-实现三级校验

- 前端存在性硬规则: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)
This commit is contained in:
hangshuo652
2026-09-01 08:44:56 +08:00
parent f8b4e9d44d
commit 1b89743d77
43 changed files with 3362 additions and 290 deletions
-14
View File
@@ -1,14 +0,0 @@
const db = require('./dist/db').default || require('./dist/db');
const projectId = 'b1b5884e-ba85-4d8f-9a92-e524603587a0';
// All standards for this project
const all = db.prepare('SELECT id, name, category_tag FROM standards WHERE project_id = ?').all(projectId);
console.log('ALL STANDARDS:', JSON.stringify(all, null, 2));
// Try exact match with empty string
const m1 = db.prepare('SELECT id, name FROM standards WHERE project_id = ? AND category_tag = ?').get(projectId, '');
console.log('MATCH EMPTY:', m1);
// Try exact match with empty string as first param
const m2 = db.prepare('SELECT id, name FROM standards WHERE project_id = ? AND (category_tag = ? OR category_tag IS NULL)').get(projectId, '');
console.log('MATCH EMPTY OR NULL:', m2);
+115 -76
View File
@@ -1,93 +1,101 @@
### 1. 场景价值与技术合理性(10分)
### 1. 场景价值与技术合理性(15分)
**前置门槛(真实性锚点,2026-08-27):本维度得分不得超过「实现完整度与稳定性」维度得分。** 系统跑不起来/实现不完整时,场景再动人也无效——场景价值必须建立在真实可运行的系统之上。
检查以下4项:
1. 真实需求(3分)
1. 真实需求(4分)
- 解决的是真实业务需求还是虚构场景
- 有明确的行业/用户场景
2. Agent不可替代性(3分)
2. Agent不可替代性(4分)
- 为什么非用Agent不可,不是传统脚本/工具能解决的
- Agent的自主决策/工具调用/多步推理是否必要
3. ROI可量化(2分)
- 效率提升/成本降低等有数据支撑
3. ROI可量化(4分)
- 效率提升/成本降低等有数据支撑(优先引用「效果与数据」维度的真实测试/基准数据)
- 效果的量化指标明确
4. 场景文档完整性(2分)
4. 场景文档完整性(3分)
- 业务背景、痛点分析、方案对比齐全
- 需求文档结构完整
> 无场景文档 → 0分
> 系统构建失败 → 本维度最高只得 5 分(真实性问题:场景价值未落地为可运行系统)
---
### 2. 开发范式应用(5分)
### 2. 开发范式应用(8分)
检查以下3项:
1. 开发流程覆盖(2分)
1. 开发流程覆盖(3分)
- AI日志是否覆盖需求分析→设计→编码→测试的完整流程
- 流程各阶段有明确记录
2. 设计文档质量(2分)
2. 设计文档质量(3分)
- 是否有需求分析、架构设计、接口设计文档
- 文档之间逻辑一致
3. 测试文档质量(1分)
3. 测试文档质量(2分)
- 是否有测试用例、测试计划、测试报告
- 测试结果可复现
> 没有任何一个维度的证据 → 0分
> 系统构建失败 → 本维度最高只得 3 分(开发范式须以真实可运行系统为落点)
---
### 3. 架构设计(10分)
### 3. 架构设计(15分)
架构文档存在性 + 代码反向推断。
1. 架构文档存在且质量高(3分)
**前置门槛(真实性锚点,2026-08-27):本维度得分不得超过「实现完整度与稳定性」维度得分。** 架构设计必须与实际可运行代码一致——代码无法构建/运行,则架构只是纸面设计。
1. 架构文档存在且质量高(4分)
- 有 DESIGN.md / docs/design.md 等完整文档
- 包含系统模块划分、组件关系、数据流
2. 模块化与分层(3分)
2. 模块化与分层(4分)
- 代码是否按职责分层、模块间依赖是否合理
- 高内聚低耦合
3. 数据流清晰度(2分)
3. 数据流清晰度(4分)
- 数据流转路径是否可追溯
- 状态管理一致
4. 可扩展性(2分)
4. 可扩展性(3分)
- 是否有接口抽象、插件机制等便于扩展的设计
- 预留扩展点
**文档规则:**
- 有完整架构文档:可评至满分10
- 无文档但有代码证据:封顶5分(允许根据代码结构反向推断模块/分层/数据流)
- 无文档且代码混乱:1-3
- 有完整架构文档且系统可构建运行:可评至满分15
- 无文档但有代码证据(系统可构建):封顶8分(允许根据代码结构反向推断模块/分层/数据流)
- 无文档且代码混乱:1-4
- 系统构建失败:本维度最高只得 5 分(架构真实性存疑)
---
### 4. 工具使用与Skill集成深度(5分)
### 4. 工具使用与Skill集成深度(10分)
AI框架集成深度 + 开发工具链。
**AI框架集成(3分):**
**AI框架集成(6分):**
- 无框架使用 → 0分
- 使用框架基本功能(chain/pipeline)→ 1
- 实现了MCP/Function Calling协议 → 2
- 自定义Agent工具链、有深度框架定制 → 3
- 使用框架基本功能(chain/pipeline)→ 2
- 实现了MCP/Function Calling协议 → 4
- 自定义Agent工具链、有深度框架定制 → 6
**开发工具链(2分):**
- IDE集成(VSCode插件/LSP/Webview面板等)→ 1
- CI/CD配置(GitHub Actions/Jenkins等)、监控/可观测性 → 1
**开发工具链(4分):**
- IDE集成(VSCode插件/LSP/Webview面板等)→ 2
- CI/CD配置(GitHub Actions/Jenkins等)、监控/可观测性 → 2
> 两项可叠加,上限5
> 两项可叠加,上限10
> 系统构建失败 → 本维度最高只得 3 分(工具链未落地为可运行成果)
---
### 5. Agent核心能力(25分)
### 5. Agent核心能力(30分)
**4项硬性门槛条件(二进制判定,缺任意1个→整个维度0分):**
@@ -111,62 +119,75 @@ AI框架集成深度 + 开发工具链。
| 评分项 | 分值 | 评价基准 |
|:-------|:---:|---------|
| Agent存在性 | 4分 | 满足4个门槛条件→4分,缺任意1个→整个维度0分 |
| 工具调用能力 | 6分 | 静态if-else工具选择→3分;动态prompt决策/多工具编排→6分 |
| 自主规划能力 | 5分 | 有任务分解(script/model/Agent call)→3分;递归/动态重规划→5分 |
| 协作机制 | 3分 | 多Agent通信(消息总线/共享memory)→3分;自主任务分配/协商→加分 |
| 可靠性 | 4分 | retry+timeout→2分;fallback+降级策略→4分 |
| Agent存在性 | 5分 | 满足4个门槛条件→5分,缺任意1个→整个维度0分 |
| 工具调用能力 | 8分 | 静态if-else工具选择→4分;动态prompt决策/多工具编排→8分 |
| 自主规划能力 | 6分 | 有任务分解(script/model/Agent call)→4分;递归/动态重规划→6分 |
| 协作机制 | 4分 | 多Agent通信(消息总线/共享memory)→4分;自主任务分配/协商→加分 |
| 可靠性 | 7分 | retry+timeout→3分;fallback+降级策略→7分 |
> 所有评分必须引用具体代码文件和行号
> 系统构建失败 → 本维度最高只得 10 分(Agent 能力须以可运行系统为载体)
---
### 6. 实现完整度与稳定性(20分)
### 6. 实现完整度与稳定性(35分)
**评分优先锚定确定性证据(2026-08-27):以下证据为系统客观检测所得,评分必须据此,不得被文档/README 声称覆盖:**
- 构建结果(系统真实执行构建命令)
- 启动/浏览结果(服务真实可访问)
- 验收命令(README `## 验收` 块真实执行)
- 功能发现轮(Map-Reduce 全量扫描:真实/部分/占位功能点统计)
检查以下5项:
1. 功能完整性(8分)
1. 功能完整性(12分)
- **功能发现轮真实功能数**:≥10项→12分;5~9项→8分;1~4项→4分;0项→0分
- 核心功能路径是否完整可运行
- 题目要求的全部功能是否实现
- 占位(stub)功能>30%→本项扣半;>50%→本项0分
2. 构建可运行(4分)
- 项目能否正常构建(npm install/pip install/mvn compile等
- 构建报错
2. 构建可运行(8分)
- 系统真实构建成功→8分;构建失败→0分(本项不证伪,未检测到构建系统视为构建失败
- 构建报错→按报错程度扣分
3. 服务可启动(3分)
- 能否正常启动服务
- README步骤可直接运行
3. 服务可启动(7分)
- 服务真实启动且可访问→7分;无法访问→0分
- 提交了 service_url 且评审期可访问→满分;CLI 形态服务可启动→满分
4. 重试/降级机制(3分)
4. 重试/降级机制(5分)
- LLM调用是否有超时处理和重试
- 是否有降级方案
5. 错误处理(2分)
5. 错误处理(3分)
- 异常输入是否有明确提示
- 错误信息是否友好
> **硬性封顶(系统确定性判定):构建失败 → 本维度最高只得 10 分。**
---
### 7. 规模与功能点(20分)
### 7. 规模与功能点(25分)
**评分优先锚定确定性证据(2026-08-27):功能发现轮统计(真实/部分/占位功能点)为规模判定的主要依据,README 声称仅作参考。**
检查以下4项:
1. 代码规模(5分)
- 基准分(每500行有效代码→0.5分,最多3分)
1. 代码规模(6分)
- 基准分(每500行有效代码→0.5分,最多4分)
- 语言多样性(3种以上语言→2分,1-2种→1分)
2. 功能点覆盖(6分)
- 核心功能完整度
- 功能复杂度(CRUD vs 复杂业务逻辑 vs 算法实现)
- 重复代码>30%→扣3分,>50%→扣全部6分
2. 功能点覆盖(10分)
- **功能发现轮真实功能数**:≥10项→10分;5~9项→7分;1~4项→3分;0项→0分
- 占位(stub)功能>30%→本项扣半;>50%→本项最高只给满分一半
- 核心功能完整度(对照 README/验收基准逐项核对功能发现清单)
- 重复代码>30%→扣3分,>50%→扣全部
3. 可演示性(2分)
- 有启动配置(Dockerfile/scripts.start)→1
- 有Web/CLI演示入口 →1
3. 可演示性(5分)
- 有启动配置(Dockerfile/scripts.start)→2
- 有Web/CLI演示入口 →3
4. 数据与测试覆盖(2分)
- 有测试数据/样本 →1
- 有测试覆盖且通过 →1
4. 数据与测试覆盖(4分)
- 有测试数据/样本 →2
- 有测试覆盖且通过 →2
---
@@ -199,11 +220,13 @@ AI框架集成深度 + 开发工具链。
---
### 9. 演示与文档(10分)
### 9. 演示与文档(8分)
**前置约束(2026-08-27):本维度考察文档质量本身,但不得超过「实现完整度与稳定性」维度得分——文档描述必须与真实可运行系统一致,假文档不得得高分。**
检查以下4项:
1. README完整性(3分)
1. README完整性(2分)
- 是否有README.md(若无→0分)
- 是否包含:项目说明、安装步骤、使用示例
- 是否包含:技术栈、依赖说明
@@ -215,63 +238,77 @@ AI框架集成深度 + 开发工具链。
3. 启动与构建说明(2分)
- 是否有明确的构建/启动命令
- 是否有环境要求说明
- **README 声称的构建/启动命令与系统真实构建结果一致**(构建失败而 README 声称可运行 → 本项0分)
4. 文档一致性(3分)
4. 文档一致性(2分)
- 文档描述与实际代码结构一致
- 无过期/废弃文档
> 系统构建失败 → 本维度最高只得 3 分(文档所述与系统真实状态不符)
---
### 10. AI使用日志(5分)
### 10. AI使用日志(10分)
**优先锚定确定性审计(2026-08-27):系统对日志做占位率审计(占位>50%→本维度封顶),评分必须参考审计结果。**
检查以下3项:
1. AI使用记录(2分)
- 有CLAUDE.md/AGENTS.md等文件记录AI协作方式 → 2
- 仅有skill/agent配置但无使用记录 → 1
1. AI使用记录(4分)
- 有CLAUDE.md/AGENTS.md等文件记录AI协作方式 → 4
- 仅有skill/agent配置但无使用记录 → 2
- 完全无任何AI相关文件 → 0分
2. 调用细节(2分)
2. 调用细节(4分)
- 记录了每次AI调用的时间、模型、目的
- 记录了prompt原文
3. 真实性验证(1分)
3. 真实性验证(2分)
- 日志内容与代码提交历史一致
- 无伪造/编造的日志条目
【交叉验证规则】
- AGENTS.md声称的"使用了XX技术",必须在代码中找到对应的配置或实现文件,否则扣2分
- 声称"调用细节记录了prompt原文"但文件内容为空或只有模板文案 → 视为不实,扣2分
- **系统确定性审计:日志占位率>50% → 本维度封顶至满分的30%**
---
### 11. 效果与数据(20分)
### 11. 效果与数据(25分)
**评分优先锚定确定性证据(2026-08-27):系统真实运行测试为准,README 自报数据不作数。**
检查以下5项:
1. 测试覆盖(5分)
1. 测试覆盖(6分)
- 是否有单元测试(若无→0分)
- 测试是否覆盖核心功能路径
- **系统真实执行测试的结果(通过数/总用例)为评分依据,无真实测试结果→本项0分**
2. 测试工具与框架(3分)
2. 测试工具与框架(4分)
- 是否使用标准测试框架(pytest jest, JUnit等)
- 是否有自动化测试配置(CI、pre-commit等)
3. 效果验证数据(4分)
- 是否有性能基准、正确性验证数据
3. 效果验证数据(5分)
- 是否有性能基准、正确性验证数据(系统真实测得)
- 是否有对比数据(如AI生成vs手写对比)
- **纯增益/提效类指标必须有基线对比数据,仅自报无基线→C档封顶**
4. 覆盖率报告(4分)
4. 覆盖率报告(5分)
- 是否有覆盖率报告(如gcov coverage.py, jest --coverage
- 覆盖率≥80%→4分,≥50%→2分,<50%→0分
- **系统真实解析的覆盖率**≥80%→5分,≥50%→3分,<50%→1分,无→0分
5. 测试结果可复现(4分)
5. 测试结果可复现(5分)
- 测试环境配置明确
- 测试数据随仓库提供(非外部依赖)
- **系统可重复执行测试且通过→5分;测试失败→2分;不可运行→0分**
> **硬性封顶(系统确定性判定):测试执行失败 → 本维度最高只得满分的50%;构建失败 → 最高只得 8 分。**
> **效果类数据缺位(无测试/无覆盖率/无基准)→ C档封顶至满分的30%。**
---
### 12. 安全性(10分)
### 12. 安全性(9分)
检查以下4项:
@@ -287,10 +324,12 @@ AI框架集成深度 + 开发工具链。
- 日志中不输出敏感信息
- 错误页面不暴露内部路径
4. 依赖安全(2分)
4. 依赖安全(1分)
- 无已知漏洞的依赖
- 依赖版本明确
> 系统构建失败 → 本维度最高只得 3 分(无法验证运行期安全性)
---
## 合格判定
+1 -1
View File
@@ -173,7 +173,7 @@ describe('Standards API', () => {
it('TC-STD-03: should reject total > max', async () => {
const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, {
name: '超分', content: '## 维度一(100分)\nx\n## 维度二(80分)\nx',
name: '超分', content: '## 维度一(100分)\nx\n## 维度二(150分)\nx',
});
expect(status).toBe(400);
expect(data.error).toContain('超过');
@@ -0,0 +1,159 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { computeClaimConsistency, renderClaimConsistencyText, buildConsistencyFromVerdicts } from '../services/claim-consistency';
// mock callDeepSeek 验证运行时证据注入(2026-08-31 增强)
vi.mock('../services/deepseek', () => ({
callDeepSeek: vi.fn(),
}));
import { callDeepSeek } from '../services/deepseek';
const mockCall = callDeepSeek as unknown as ReturnType<typeof vi.fn>;
const inventory = [
{ feature: '支持PDF文档解析', depth: 'real' },
{ feature: '生成Word报告', depth: 'real' },
{ feature: '多语言翻译', depth: 'real' },
{ feature: '用户登录鉴权', depth: 'stub' },
{ feature: '知识库检索', depth: 'partial' },
];
describe('TC-CLAIM · 文档声称-代码实现一致性(2026-08-27', () => {
it('声称全部与代码真实实现一致 → 一致性高(可信)', () => {
const claims = [
{ feature: 'PDF文档解析', source: 'README.md' },
{ feature: 'Word报告生成', source: 'README.md' },
{ feature: '多语言翻译功能', source: 'DESIGN.md' },
];
const r = computeClaimConsistency(claims, inventory)!;
expect(r.claimsCount).toBe(3);
expect(r.consistencyRatio).toBeGreaterThanOrEqual(0.9);
expect(r.reliable).toBe(true);
expect(r.fakeClaims.length).toBe(0);
});
it('声称了代码中没有的功能 → 一致性低(虚报)', () => {
const claims = [
{ feature: 'PDF文档解析', source: 'README.md' },
{ feature: '区块链存证', source: 'README.md' }, // 代码中没有
{ feature: '自动驾驶控制', source: 'README.md' }, // 代码中没有
{ feature: '人脸识别支付', source: 'README.md' }, // 代码中没有
];
const r = computeClaimConsistency(claims, inventory)!;
expect(r.consistencyRatio).toBeLessThan(0.6);
expect(r.reliable).toBe(false);
expect(r.fakeClaims).toContain('区块链存证');
expect(r.fakeClaims.length).toBe(3);
});
it('声称了占位(stub)功能 → 虚报', () => {
const claims = [{ feature: '用户登录鉴权', source: 'README.md' }]; // inventory 中 stub
const r = computeClaimConsistency(claims, inventory)!;
expect(r.consistencyRatio).toBe(0);
expect(r.fakeClaims).toContain('用户登录鉴权');
});
it('部分实现 → 算 0.5 权重', () => {
const claims = [{ feature: '知识库检索', source: 'README.md' }]; // partial
const r = computeClaimConsistency(claims, inventory)!;
expect(r.consistencyRatio).toBe(0.5);
expect(r.fakeClaims.length).toBe(0);
// 0.5 < 0.6 → 不算"基本可信",也不算虚报(无 fakeClaims);介于中间
expect(r.partiallyReliable).toBe(false);
expect(r.reliable).toBe(false);
});
it('空声称 → null', () => {
expect(computeClaimConsistency([], inventory)).toBeNull();
});
it('无库存(无代码)→ 全虚报', () => {
const claims = [{ feature: 'PDF解析', source: 'README.md' }];
const r = computeClaimConsistency(claims, [])!;
expect(r.consistencyRatio).toBe(0);
expect(r.fakeClaims.length).toBe(1);
});
it('渲染文本含评分绑定规则(虚报→封顶50%)', () => {
const claims = [
{ feature: 'PDF文档解析', source: 'README.md' },
{ feature: '区块链存证', source: 'README.md' },
{ feature: '自动驾驶', source: 'README.md' },
];
const r = computeClaimConsistency(claims, inventory)!;
const t = renderClaimConsistencyText(r);
expect(t).toContain('一致性率 33%');
expect(t).toContain('文档类维度封顶至满分 50%');
expect(t).toContain('区块链存证');
expect(t).toContain('评分必须据此');
});
it('高一致性渲染文本 → 文档可信', () => {
const claims = [
{ feature: 'PDF文档解析', source: 'README.md' },
{ feature: 'Word报告生成', source: 'README.md' },
{ feature: '多语言翻译', source: 'README.md' },
];
const r = computeClaimConsistency(claims, inventory)!;
const t = renderClaimConsistencyText(r);
expect(t).toContain('文档声称与代码实现高度一致');
expect(t).not.toContain('封顶');
});
});
describe('TC-CLAIM-RUNTIME · 运行时证据增强(2026-08-31', () => {
beforeEach(() => { mockCall.mockReset(); });
it('verifyClaimsByAI 注入运行时证据(构建失败/测试失败/前端不可访问)到 prompt', async () => {
const { verifyClaimsByAI } = await import('../services/claim-consistency');
mockCall.mockResolvedValue(JSON.stringify([
{ feature: '提供Web上传界面', verdict: 'partial', evidence: 'web/api.py 存在但服务无法启动' },
{ feature: '生成测试报告', verdict: 'real', evidence: 'report/ 存在' },
]));
const claims = [
{ feature: '提供Web上传界面', source: 'README.md' },
{ feature: '生成测试报告', source: 'README.md' },
];
const runtime = {
buildOk: false,
buildSummary: '构建失败',
testsPassed: false,
testSummary: '830用例7处导入错误',
webAccessible: false,
webSummary: '服务启动报错 check_coverage 不存在',
};
const verdicts = await verifyClaimsByAI(claims, inventory, runtime);
expect(verdicts).not.toBeNull();
expect(verdicts![0].verdict).toBe('partial'); // 代码存在但运行验证失败 → 不得 real
// 验证 prompt 含运行时证据
const prompt = mockCall.mock.calls[0][0];
expect(prompt).toContain('运行验证结果');
expect(prompt).toContain('构建:失败');
expect(prompt).toContain('测试:失败/无法运行');
expect(prompt).toContain('前端/Web服务:无法访问');
expect(prompt).toContain('运行验证失败的功能不得判 real');
});
it('verifyClaimsByAI 无运行时证据 → prompt 不含运行验证段落', async () => {
const { verifyClaimsByAI } = await import('../services/claim-consistency');
mockCall.mockResolvedValue(JSON.stringify([
{ feature: 'PDF文档解析', verdict: 'real', evidence: 'parse.py' },
]));
const verdicts = await verifyClaimsByAI([{ feature: 'PDF文档解析', source: 'README.md' }], inventory);
expect(verdicts![0].verdict).toBe('real');
const prompt = mockCall.mock.calls[0][0];
expect(prompt).not.toContain('运行验证结果');
});
it('buildConsistencyFromVerdicts:运行验证失败的功能判 partial → 一致性率降权', () => {
const claims = [
{ feature: 'Web上传界面', source: 'README.md' },
{ feature: 'PDF解析', source: 'README.md' },
];
const verdicts = [
{ feature: 'Web上传界面', verdict: 'partial' as const }, // 存在但运行失败
{ feature: 'PDF解析', verdict: 'real' as const },
];
const r = buildConsistencyFromVerdicts(claims, verdicts);
expect(r.consistencyRatio).toBe(0.75); // 0.5 + 1 / 2
expect(r.partiallyReliable).toBe(true); // 0.75 ∈ [0.6, 0.9)
});
});
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, expect } from 'vitest';
import { computeCodeQuality } from '../services/code-quality';
const mk = (path: string, content: string) => ({ path, content });
describe('TC-QUALITY · 代码质量确定性评估(2026-08-27', () => {
it('真实实现(大量逻辑、无占位)→ 质量分高', () => {
const files = [
mk('src/core.py', [
'def process(data):',
' result = []',
' for item in data:',
' if item["ok"]:',
' result.append(item["value"] * 2)',
' else:',
' result.append(0)',
' return result',
'',
'def analyze(x):',
' return {"count": len(x), "total": sum(x)}',
].join('\n')),
mk('tests/test_core.py', [
'def test_process():',
' assert process([{"ok": True, "value": 3}]) == [6]',
'def test_analyze():',
' assert analyze([1,2,3]) == {"count": 3, "total": 6}',
].join('\n')),
];
const r = computeCodeQuality(files);
expect(r.applicable).toBe(true);
expect(r.stubRatio).toBe(0); // 无空壳
expect(r.qualityScore).toBeGreaterThanOrEqual(60);
expect(r.testValidityRatio).toBe(0.5); // 2断言/2测试函数 = 0.5
});
it('空壳函数多(pass/占位)→ 空壳率升高、质量分低', () => {
const files = [
mk('src/core.py', [
'def a():',
' pass',
'def b():',
' pass',
'def c():',
' pass',
'def real():',
' return 42',
].join('\n')),
];
const r = computeCodeQuality(files);
expect(r.applicable).toBe(true);
expect(r.stubRatio).toBeGreaterThan(0.5); // 3/4 空壳
expect(r.qualityScore).toBeLessThan(40);
});
it('重复代码多 → 重复率升高', () => {
const dupBlock = [
' result = []',
' for item in items:',
' if item["ok"] and item["val"] > threshold:',
' result.append(item["val"] * 2 + offset - tax_rate)',
' else:',
' result.append(0)',
' return result',
].join('\n');
const files = [];
for (let i = 0; i < 5; i++) {
files.push(mk(`src/mod${i}.py`, `def f${i}(items, offset, tax_rate, threshold):\n${dupBlock}\n`));
}
const r = computeCodeQuality(files);
expect(r.applicable).toBe(true);
expect(r.dupRatio).toBeGreaterThan(0.5);
});
it('测试只测边角料(无断言)→ 测试有效度低', () => {
const files = [
mk('src/core.py', 'def real(x):\n return x * 2\n'),
mk('tests/test_core.py', [
'def test_placeholder():',
' pass',
'def test_other():',
' return',
].join('\n')),
];
const r = computeCodeQuality(files);
expect(r.applicable).toBe(true);
expect(r.testValidityRatio).toBe(0); // 无断言
});
it('无源码文件 → 不适用', () => {
const r = computeCodeQuality([mk('README.md', '# doc')]);
expect(r.applicable).toBe(false);
});
it('toPrompt 含三项指标', () => {
const r = computeCodeQuality([mk('src/a.py', 'def x():\n return 1\n')]);
expect(r.toPrompt).toContain('空壳率');
expect(r.toPrompt).toContain('重复率');
expect(r.toPrompt).toContain('测试有效度');
expect(r.toPrompt).toContain('确定性证据');
});
});
+2 -2
View File
@@ -229,8 +229,8 @@ describe('Standard Total Score Validation', () => {
it('TC-STD-TOTAL-02: should reject total > max', async () => {
const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, {
name: 'total-180',
content: '## a100分)\n## b80分)',
name: 'total-250',
content: '## a100分)\n## b150分)',
});
expect(status).toBe(400);
expect(data.error).toContain('超过');
+40 -2
View File
@@ -16,11 +16,14 @@ function scoreOf(dims: { name: string; score: number }[], name: string): number
}
describe('TC-HARD · 硬规则封顶(§3.3.7', () => {
it('TC-HARD-01: 构建失败 → 实现完整度≤floor(max×0.33)、效果与数据≤floor(max×0.30)', () => {
it('TC-HARD-01: 构建失败 → 实现完整度≤floor(max×0.33)、效果与数据≤floor(max×0.30)、纸面维度半封顶', () => {
const { dimensions } = applyHardRules(baseDims(), { buildFailed: true, testStepFailed: false, duplicateRatio: 0.1, hasAnyReadme: true, hasRootReadme: true });
expect(scoreOf(dimensions, '实现完整度')).toBe(3); // floor(12×0.33)
expect(scoreOf(dimensions, '效果与数据')).toBe(3); // floor(10×0.30)
expect(scoreOf(dimensions, '架构设计')).toBe(10); // 不受影响
// 真实性绑定(2026-08-27):架构设计属纸面维度,构建失败 → 半封顶
expect(scoreOf(dimensions, '架构设计')).toBe(3); // floor(10×0.33)
expect(scoreOf(dimensions, '代码规范')).toBe(1); // floor(5×0.33)
expect(scoreOf(dimensions, '演示与文档')).toBe(1); // floor(5×0.33)
});
it('TC-HARD-02: 非构建失败(含 untested)→ 构建维度不封顶(M2 回归)', () => {
@@ -60,4 +63,39 @@ describe('TC-HARD · 硬规则封顶(§3.3.7', () => {
expect(log[0]).toContain('效果与数据');
expect(log[0]).toContain('3');
});
it('TC-HARD-07: 真实前端缺失(web 形态无前端)→ 实现完整度≤50%,CLI/插件豁免不触发', () => {
const dims = baseDims();
// 无前端 + 非 CLI/插件 → 实现完整度封顶 50%
const absent = applyHardRules(dims, { buildFailed: false, testStepFailed: false, duplicateRatio: 0.1, hasAnyReadme: true, hasRootReadme: true, frontendAbsent: true, frontendCliOrExtension: false });
expect(scoreOf(absent.dimensions, '实现完整度')).toBe(6); // floor(12×0.5)
// 效果与数据不在此封顶(由可验证三档单独管理)
expect(scoreOf(absent.dimensions, '效果与数据')).toBe(10);
// CLI/插件豁免 → 实现完整度不封顶
const exempt = applyHardRules(dims, { buildFailed: false, testStepFailed: false, duplicateRatio: 0.1, hasAnyReadme: true, hasRootReadme: true, frontendAbsent: true, frontendCliOrExtension: true });
expect(scoreOf(exempt.dimensions, '实现完整度')).toBe(12);
});
it('TC-HARD-08: 运行验证全面失败(前端缺失+测试失败)→ 静态纸面维度≤50%,防原型拿高分', () => {
const dims = baseDims();
const r = applyHardRules(dims, { buildFailed: false, testStepFailed: true, duplicateRatio: 0.1, hasAnyReadme: true, hasRootReadme: true, frontendAbsent: true, frontendCliOrExtension: false });
// 架构设计(静态纸面)≤50%
expect(scoreOf(r.dimensions, '架构设计')).toBe(5); // floor(10×0.5)
// 代码规范(静态纸面)≤50%
expect(scoreOf(r.dimensions, '代码规范')).toBe(2); // floor(5×0.5)
// 实现完整度由前端缺失规则封顶 ≤50%(6)
expect(scoreOf(r.dimensions, '实现完整度')).toBe(6);
// 效果与数据由测试失败规则封顶 ≤50%(5)
expect(scoreOf(r.dimensions, '效果与数据')).toBe(5);
// log 应记录架构/代码规范封顶
expect(r.log.some(l => l.includes('架构设计'))).toBe(true);
});
it('TC-HARD-09: 运行验证失败但测试通过(有测试但前端缺失)→ 静态维度不压缩', () => {
const dims = baseDims();
const r = applyHardRules(dims, { buildFailed: false, testStepFailed: false, duplicateRatio: 0.1, hasAnyReadme: true, hasRootReadme: true, frontendAbsent: true, frontendCliOrExtension: false });
// 测试通过时静态维度不因运行验证失败压缩(只是实现完整度因前端缺失封顶)
expect(scoreOf(r.dimensions, '架构设计')).toBe(10);
expect(scoreOf(r.dimensions, '实现完整度')).toBe(6);
});
});
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { auditMeasurement } from '../services/measurement-audit';
function mkdirp(p: string) { fs.mkdirSync(p, { recursive: true }); }
describe('TC-MEASURE · 测量协议检查(2026-08-26 P2', () => {
it('完整协议:data/measurement/ 含 baseline+timing → 提示可对账', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msr-'));
mkdirp(path.join(tmp, 'data', 'measurement'));
fs.writeFileSync(path.join(tmp, 'data', 'measurement', 'baseline.md'), '人工审查3文件需28分钟');
fs.writeFileSync(path.join(tmp, 'data', 'measurement', 'timing-log.csv'), 'start,end,耗时');
const r = auditMeasurement(tmp);
expect(r.dirExists).toBe(true);
expect(r.hasBaseline).toBe(true);
expect(r.hasTimingLog).toBe(true);
expect(r.toPrompt).toContain('协议齐全');
fs.rmSync(tmp, { recursive: true, force: true });
});
it('缺协议:无 data/measurement → 提示按未证实处理', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msr-'));
fs.writeFileSync(path.join(tmp, 'README.md'), '提效 30 分钟');
const r = auditMeasurement(tmp);
expect(r.dirExists).toBe(false);
expect(r.hasBaseline).toBe(false);
expect(r.toPrompt).toContain('未证实');
fs.rmSync(tmp, { recursive: true, force: true });
});
});
+22 -32
View File
@@ -154,11 +154,11 @@ describe('TC-VERIFY · 阶段B人工构建确认(US-15 / US-19,§2.2/§2.5
const proj = await api('POST', '/api/projects', { name: 'verify-test', track: '赛道一' });
vProjectId = proj.data.id;
// Web 形态 fixtureindex.html + package.json → hasWeb=true
// Web 形态 fixtureindex.html + README → hasWeb=true,无 package.json → tryBuild 快速跳过,避免 npm install 拖慢测试
webFixture = path.join(fixtureRoot, 'verify-web');
fs.mkdirSync(webFixture, { recursive: true });
fs.writeFileSync(path.join(webFixture, 'index.html'), '<html><body>web app</body></html>');
fs.writeFileSync(path.join(webFixture, 'package.json'), JSON.stringify({ scripts: { dev: 'vite' }, dependencies: { react: '^18' } }));
fs.writeFileSync(path.join(webFixture, 'README.md'), '# web app\n');
fs.writeFileSync(path.join(webFixture, 'app.ts'), 'export const x = 1;\n');
// CLI 形态 fixture(无 web 信号 → hasWeb=false
@@ -189,47 +189,45 @@ describe('TC-VERIFY · 阶段B人工构建确认(US-15 / US-19,§2.2/§2.5
throw new Error('A 阶段未在期限内到达 a_done');
};
// 直接置 a_done(复用已 clone 的目录,避免重复走完整 A 阶段 + 并发槽竞争导致测试超时)
const forceADone = async (eid: string) => {
const dbMod = await import('../db');
(dbMod.default as any).prepare("UPDATE entries SET status='a_done', stage_b_status='pending' WHERE id=?").run(eid);
};
it('TC-VERIFY-01: 非 a_done 状态触发 verify → 409', async () => {
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, { build_status: 'done' });
expect(r.status).toBe(409);
});
it('TC-VERIFY-02: 缺 build_status → 400', async () => {
it('TC-VERIFY-02: 缺 build_status → 400(人工构建确认必填,2026-08-27 恢复赛制原设计)', async () => {
await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/start`);
await waitA_done(vWebEntry);
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, {});
expect(r.status).toBe(400);
expect(String(r.data.error)).toContain('构建结果');
});
}, 30000);
it('TC-VERIFY-03: build_status 非法值 → 400', async () => {
await forceADone(vWebEntry);
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, { build_status: 'maybe' });
expect(r.status).toBe(400);
});
it('TC-VERIFY-04: Web 形态构建完成未填 service_url → 400US-15 严格拦截', async () => {
it('TC-VERIFY-04: Web 形态构建完成未填 service_url → 降级放行 2002026-08-27 放宽,B 阶段跳过浏览器测试继续评审', async () => {
await forceADone(vWebEntry); // 复用 vWebEntryTC-VERIFY-02 已 cloneclone 目录保留)
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, { build_status: 'done' });
expect(r.status).toBe(400);
expect(String(r.data.error)).toContain('服务地址');
});
expect(r.status).toBe(200);
expect(r.data.success).toBe(true);
}, 30000);
it('TC-VERIFY-05: Web 形态构建完成已填 service_url → 200(进入 B 阶段)', async () => {
await forceADone(vWebEntry);
await api('PUT', `/api/projects/${vProjectId}/entries/${vWebEntry}`, { service_url: 'http://8.8.8.8:9999' });
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, { build_status: 'done' });
expect(r.status).toBe(200);
expect(r.data.success).toBe(true);
// B 阶段完成后回到 review_doneservice_url 打不开 → 中性跳过冒烟,测试证据缺失 → 中性)
const deadline = Date.now() + 60000;
let final: any = null;
while (Date.now() < deadline) {
const g = await api('GET', `/api/projects/${vProjectId}/entries/${vWebEntry}`);
if (g.data.status === 'review_done') { final = g.data; break; }
await new Promise(res => setTimeout(res, 500));
}
expect(final).toBeTruthy();
expect(final.status).toBe('review_done');
expect(final.score_b).toBeGreaterThanOrEqual(0);
}, 120000);
}, 30000);
it('TC-VERIFY-06: CLI 形态构建完成无需 service_url → 200', async () => {
await api('POST', `/api/projects/${vProjectId}/entries/${vCliEntry}/start`);
@@ -240,18 +238,10 @@ describe('TC-VERIFY · 阶段B人工构建确认(US-15 / US-19,§2.2/§2.5
}, 60000);
it('TC-VERIFY-07: 构建失败无需 service_url → 200B 阶段照跑', async () => {
await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/start`);
await waitA_done(vWebEntry);
await forceADone(vWebEntry);
const r = await api('POST', `/api/projects/${vProjectId}/entries/${vWebEntry}/verify`, { build_status: 'failed' });
expect(r.status).toBe(200);
const deadline = Date.now() + 60000;
let final: any = null;
while (Date.now() < deadline) {
const g = await api('GET', `/api/projects/${vProjectId}/entries/${vWebEntry}`);
if (g.data.status === 'review_done') { final = g.data; break; }
await new Promise(res => setTimeout(res, 500));
}
expect(final).toBeTruthy();
expect(final.status).toBe('review_done');
}, 120000);
// 人工确认失败 → 触发 B 维度封顶硬规则
expect(r.data.success).toBe(true);
}, 30000);
});
@@ -3,6 +3,7 @@ import { extractSignatures, renderInventoryText } from '../services/code-invento
import { detectStubSignals } from '../services/code-signals';
import { auditAiUsageLog, renderLogAuditToPrompt } from '../services/ai-log-audit';
import { isQualitativeDesignDim, isPureGainDim } from '../services/standard-utils';
import { buildFeatureStatsText } from '../services/review.service';
describe('TC-INV · 全量符号索引(2026-08-26', () => {
const mk = (path: string, content: string) => ({ path, content, size: content.length });
@@ -124,3 +125,26 @@ describe('TC-DIMCLASS · 判档白名单(2026-08-26', () => {
expect(isPureGainDim('提效设计合理性')).toBe(false);
});
});
describe('TC-FEATSTATS · 功能发现统计锚点(2026-08-27', () => {
it('统计真实/部分/占位数量与占比', () => {
const arr = Array(7).fill(null).map((_, i) => ({ feature: 'F' + i, depth: i < 5 ? 'real' : i < 6 ? 'partial' : 'stub' }));
const t = buildFeatureStatsText(arr);
expect(t).toContain('识别出 7 个功能点(真实 5 / 部分 1 / 占位 1,真实占比 71%');
expect(t).toContain('确定性证据,评分必须据此');
expect(t).toContain('真实功能 ≥10 项 → 覆盖完整');
});
it('占位比例高 → 降档规则出现在锚点', () => {
const arr = Array(4).fill(null).map(() => ({ feature: 'S', depth: 'stub' }));
const t = buildFeatureStatsText(arr);
expect(t).toContain('占位 4');
expect(t).toContain('>50% → 覆盖子项最高只给满分的一半');
});
it('空/非法 → 空串', () => {
expect(buildFeatureStatsText([])).toBe('');
expect(buildFeatureStatsText(null as any)).toBe('');
expect(buildFeatureStatsText('nope' as any)).toBe('');
});
});
@@ -287,6 +287,29 @@ describe('TC-AGG · 多次评审聚合(2026-08-19', () => {
];
expect(aggregateEntryScores(rows, 3)).toEqual({ value: 80, count: 1, method: 'single' });
});
it('aggregateEntryScores: 历史旧标准 + 最近新标准 → 只看最近 N 条(2026-08-27 修复)', () => {
// 6 条历史旧标准 + 最近 3 条新标准(三轮重评)→ 应聚合最近 3 条,不被历史污染
const rows = [
{ score: 73, standard_snapshot: 'OLD' },
{ score: 78, standard_snapshot: 'OLD' },
{ score: 69, standard_snapshot: 'OLD' },
{ score: 69, standard_snapshot: 'OLD' },
{ score: 66, standard_snapshot: 'OLD' },
{ score: 75, standard_snapshot: 'NEW' },
{ score: 80, standard_snapshot: 'NEW' },
{ score: 77, standard_snapshot: 'NEW' },
];
expect(aggregateEntryScores(rows, 3)).toEqual({ value: 77, count: 3, method: 'median' });
});
it('aggregateEntryScores: 最近 N 条内标准不一致仍 → null', () => {
const rows = [
{ score: 75, standard_snapshot: 'NEW' },
{ score: 80, standard_snapshot: 'NEW2' },
];
expect(aggregateEntryScores(rows, 3)).toBeNull();
});
});
describe('TC-VERIFY · 可验证能力三档(2026-08-19', () => {
@@ -409,3 +432,51 @@ describe('TC-NEUTRAL · overall 中性证据归类(2026-08-19', () => {
expect(neutralizeTestEvidence(null)).toContain('中性');
});
});
describe('TC-CHECKS · 定向追踪 checks 解析(2026-08-27 P1修正)', () => {
const dim = { name: '实现完整度', maxScore: 20 };
it('标准 JSON 解析出 checks 数组', () => {
const r = parseDimResponse(JSON.stringify({
name: '实现完整度', score: 12, comment: '部分实现',
checks: [
{ feature: '数据导入', entry: 'src/main.ts:40', depth: 'real', evidence: 'return rows', reason: '真实逻辑' },
{ feature: '报表导出', entry: 'src/report.ts:12', depth: 'stub', evidence: 'return []', reason: '空逻辑' },
],
}), dim);
expect(r.score).toBe(12);
expect(r.checks).toHaveLength(2);
expect(r.checks![0]).toMatchObject({ feature: '数据导入', entry: 'src/main.ts:40', depth: 'real' });
expect(r.checks![1].depth).toBe('stub');
});
it('代码块包裹 JSON 也能解析 checks', () => {
const r = parseDimResponse('```json\n{"name":"实现完整度","score":8,"comment":"x","checks":[{"feature":"F1","entry":"a.ts:1","depth":"partial","evidence":"e","reason":"r"}]}\n```', dim);
expect(r.checks).toHaveLength(1);
expect(r.checks![0].depth).toBe('partial');
});
it('checks 含非法 depth 被过滤', () => {
const r = parseDimResponse(JSON.stringify({
name: '实现完整度', score: 5, comment: 'c',
checks: [
{ feature: '合法', entry: 'a:1', depth: 'real', evidence: '', reason: '' },
{ feature: '非法', entry: 'b:2', depth: 'maybe', evidence: '', reason: '' },
'非对象',
],
}), dim);
expect(r.checks).toHaveLength(1);
expect(r.checks![0].feature).toBe('合法');
});
it('无 checks 字段 → checks undefined(不破坏现有行为)', () => {
const r = parseDimResponse('{"name":"实现完整度","score":3,"comment":"c","suggestion":"s"}', dim);
expect(r.checks).toBeUndefined();
expect(r.score).toBe(3);
});
it('解析失败回退正则,checks undefined', () => {
const r = parseDimResponse('not json at all', dim);
expect(r.checks).toBeUndefined();
expect(r.comment).toBe('解析失败');
});
});
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { tryTest } from '../services/test-runner';
// 临时 Python 项目:验证 tryTest 自动装依赖后能跑通测试
function mkTmpPy(hasRequirements: boolean): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tt-py-'));
fs.writeFileSync(path.join(dir, 'pyproject.toml'), `[build-system]\nrequires = ["setuptools"]\nbuild-backend = "setuptools.build_meta"\n`);
fs.writeFileSync(path.join(dir, 'test_sample.py'), 'def test_ok():\n assert 1 == 1\n');
return dir;
}
describe('tryTest 依赖预装(2026-08-31', () => {
it('Python 项目跑测试前尝试安装依赖(失败中性,不阻断)', async () => {
const dir = mkTmpPy(true);
try {
const r = await tryTest(dir);
// 不依赖安装是否成功——只要不抛异常且返回结构正确即可
expect(r).toHaveProperty('tested');
expect(r).toHaveProperty('summary');
expect(typeof r.summary).toBe('string');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}, 60000);
});
+7 -2
View File
@@ -119,7 +119,7 @@ describe('US-04 · 自拟选题(A0/B0)赛道标准', () => {
return lines.filter(l => /^###\s+\d+\.\s+\S+\d+分)/.test(l.trim()));
};
it('TC-US04-1: 赛道一标准 12 维 / 总分 150', () => {
it('TC-US04-1: 赛道一标准 12 维 / 总分 2002026-08-27 真实性优先重构,功能类 85 分)', () => {
const stdPath = path.resolve(__dirname, '../../config/standards/技术大赛-赛道一.md');
const md = fs.readFileSync(stdPath, 'utf-8');
const dims = topDims(md);
@@ -128,7 +128,12 @@ describe('US-04 · 自拟选题(A0/B0)赛道标准', () => {
const m = l.match(/(\d+)分)/);
return s + (m ? parseInt(m[1], 10) : 0);
}, 0);
expect(total).toBe(150);
expect(total).toBe(200);
// 功能真实类维度(实现完整度/规模/效果)合计 ≥80,文档/规范/安全类(演示/代码规范/安全)合计 ≤30
const jy = dims.filter(l => /实现完整|规模|效果/.test(l)).reduce((s, l) => { const m = l.match(/(\d+)分)/); return s + (m ? parseInt(m[1], 10) : 0); }, 0);
const wd = dims.filter(l => /演示与文档|代码规范|安全性/.test(l)).reduce((s, l) => { const m = l.match(/(\d+)分)/); return s + (m ? parseInt(m[1], 10) : 0); }, 0);
expect(jy).toBeGreaterThanOrEqual(80);
expect(wd).toBeLessThanOrEqual(30);
});
it('TC-US04-2: 赛道二标准 8 维 / 总分 100', () => {
+55 -1
View File
@@ -7,7 +7,7 @@ import path from 'path';
const tmpDb = path.join(os.tmpdir(), `ai-review-wm-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
process.env.DB_PATH = tmpDb;
import { detectWebMode, resolveWebMode } from '../services/review.service';
import { detectWebMode, resolveWebMode, detectFrontend } from '../services/review.service';
import db from '../db';
let webDir = '';
@@ -177,3 +177,57 @@ describe('resolveWebMode(§2.7.1 三态:确定性优先,模糊交 AI)',
expect(r.crossMismatch).toBe(true);
});
});
describe('detectFrontend(§2.7.2 真实前端判定,2026-08-31', () => {
it('index.html + react → 有真实前端', () => {
const r = detectFrontend(webDir);
expect(r.hasFrontend).toBe(true);
expect(r.cliOrExtension).toBe(false);
expect(r.reason).toContain('真实前端');
});
it('FastAPI + templates/index.html → 有真实前端', () => {
const r = detectFrontend(fastapiDir);
expect(r.hasFrontend).toBe(true);
expect(r.cliOrExtension).toBe(false);
});
it('纯 Go CLI → CLI 豁免(不算无前端,不触发封顶)', () => {
const r = detectFrontend(cliDir);
expect(r.hasFrontend).toBe(true); // 豁免视为有前端(不触发封顶)
expect(r.cliOrExtension).toBe(true);
});
it('纯 Python 库(无前端/无CLI信号)→ 无真实前端', () => {
const r = detectFrontend(ambiguousDir);
expect(r.hasFrontend).toBe(false);
expect(r.cliOrExtension).toBe(false);
expect(r.reason).toContain('未发现真实前端');
});
it('coverage 产物 index.html 不算真实前端', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontend-cover-'));
try {
fs.mkdirSync(path.join(dir, 'coverage'), { recursive: true });
fs.writeFileSync(path.join(dir, 'coverage', 'index.html'), '<html>cov</html>');
fs.writeFileSync(path.join(dir, 'app.py'), 'from fastapi import FastAPI\napp = FastAPI()\n');
const r = detectFrontend(dir);
expect(r.hasFrontend).toBe(false); // 只有 coverage 产物,无真实前端源码
expect(r.cliOrExtension).toBe(false);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('VS Code 插件(package.json contributes)→ 豁免', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontend-ext-'));
try {
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ contributes: { commands: [] } }));
const r = detectFrontend(dir);
expect(r.hasFrontend).toBe(true);
expect(r.cliOrExtension).toBe(true);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});
+3 -1
View File
@@ -58,7 +58,9 @@ export const config = {
allowLocalServiceUrl: process.env.ALLOW_LOCAL_SERVICE_URL === '1',
giteaToken: process.env.GITEA_TOKEN || '',
giteaUsername: process.env.GITEA_USERNAME || '',
standardMaxScore: parseInt(process.env.STANDARD_MAX_SCORE || '150', 10),
standardMaxScore: parseInt(process.env.STANDARD_MAX_SCORE || '200', 10),
// 多次评审聚合窗口(2026-08-27:3 → 2,评审成本减半、仍取中位数抗单次误判)
aggregateRounds: parseInt(process.env.AGGREGATE_ROUNDS || '2', 10),
};
if (!rawPassword) {
+4
View File
@@ -109,6 +109,10 @@ try { db.exec("ALTER TABLE entries ADD COLUMN selected_topic TEXT DEFAULT ''");
try { db.exec("ALTER TABLE entries ADD COLUMN self_registration TEXT DEFAULT ''"); } catch (e) {}
// L2考核查重初筛(2026-08-23):跨仓库相似 flags 落库(确定性检测,仅告警不定罪)
try { db.exec("ALTER TABLE entries ADD COLUMN plagiarism_json TEXT DEFAULT ''"); } catch (e) {}
// 全量功能发现(2026-08-27 P1修正):Map-Reduce 功能发现轮结果落库(供详情页展示与跨快照复用)
try { db.exec("ALTER TABLE entries ADD COLUMN feature_inventory TEXT DEFAULT ''"); } catch (e) {}
// 文档声称-代码实现一致性(2026-08-27):声称功能清单 ↔ 功能发现轮比对结果落库(供硬规则/详情页)
try { db.exec("ALTER TABLE entries ADD COLUMN claim_consistency_json TEXT DEFAULT ''"); } catch (e) {}
// backfill:历史快照 score 为 NULL 时从 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`);
+51 -14
View File
@@ -85,13 +85,15 @@ router.get('/', (req: Request, res: Response) => {
params.push(pageLimit, pageOffset);
const items = db.prepare(sql).all(...params) as any[];
// 多次评审聚合(2026-08-19):附加聚合分 / 次数 / 正式标记(<3 次为初评)
// 多次评审聚合(2026-08-19):附加聚合分 / 次数 / 正式标记(<N 次为初评N=config.aggregateRounds
const snapshots = loadEntrySnapshotScores(items.map(i => i.id));
const ROUNDS = config.aggregateRounds;
for (const it of items) {
const agg = aggregateEntryScores(snapshots[it.id] || [], 3);
it.aggregate_score = agg && agg.count >= 3 ? agg.value : null;
const agg = aggregateEntryScores(snapshots[it.id] || [], ROUNDS);
// 聚合分(count≥N)或回退最新 final_score;null 聚合(如空仓库单次0分)回退 final_score
it.aggregate_score = agg && agg.count >= ROUNDS ? agg.value : (it.final_score ?? it.raw_score ?? null);
it.aggregate_count = agg ? agg.count : 0;
it.is_formal = !!(agg && agg.count >= 3);
it.is_formal = !!(agg && agg.count >= ROUNDS);
}
res.json({ items, total, offset: pageOffset, limit: pageLimit });
@@ -133,7 +135,16 @@ router.get('/:entryId', (req: Request, res: Response) => {
}
}
res.json({ ...entry, dimensions, dimsAgg, revisions, snapshots });
// 详情页聚合分(与列表一致,2026-08-27):供前端"最终结果"展示
const ROUNDS_DETAIL = config.aggregateRounds;
const aggDetail = aggregateEntryScores(snapshots.map(s => ({ score: s.score, standard_snapshot: s.standard_snapshot })), ROUNDS_DETAIL);
const aggregate_score = aggDetail && aggDetail.count >= ROUNDS_DETAIL
? aggDetail.value
: (entry.final_score ?? entry.raw_score ?? null);
const aggregate_count = aggDetail ? aggDetail.count : 0;
const is_formal = !!(aggDetail && aggDetail.count >= ROUNDS_DETAIL);
res.json({ ...entry, dimensions, dimsAgg, revisions, snapshots, aggregate_score, aggregate_count, is_formal });
});
const dnsLookup = promisify(dns.lookup);
@@ -406,30 +417,54 @@ router.post('/:entryId/start', (req: Request, res: Response) => {
res.json({ success: true, rounds });
});
// §2.5 阶段 B 触发端点:a_done → 接收 build_statusdone/failed)→ hasWeb 校验 service_url → startReviewB(复用 queue,受 MAX_CONCURRENT
// §2.5 阶段 B 触发端点:a_done → 接收 build_statusdone/failed/done_no_ui)→ hasWeb 校验 service_url → startReviewB(复用 queue,受 MAX_CONCURRENT
router.post('/:entryId/verify', (req: Request, res: Response) => {
if (!verifyProject(pid(req), res)) return;
const entry = db.prepare('SELECT * FROM entries WHERE id = ? AND project_id = ?').get(eid(req), pid(req)) as any;
if (!entry) return res.status(404).json({ error: '条目不存在' });
if (entry.status !== 'a_done') return res.status(409).json({ error: `当前状态(${entry.status})不允许启动系统验证` });
// §2.2 人工构建确认:build_status 必填且必须为 done/failed
const buildStatus = String(req.body?.build_status || '').trim() as 'done' | 'failed';
if (buildStatus !== 'done' && buildStatus !== 'failed') {
return res.status(400).json({ error: '构建结果必须为 done 或 failed' });
// §2.2 人工构建确认(赛制原设计,2026-08-27 恢复)build_status 必填且必须为 done/failed/done_no_ui
// 由评委/选手实际构建后确认,B 阶段以人工确认结果为最终判定(不封顶/封顶/前端缺失封顶)
const buildStatus = String(req.body?.build_status || '').trim() as 'done' | 'failed' | 'done_no_ui';
if (buildStatus !== 'done' && buildStatus !== 'failed' && buildStatus !== 'done_no_ui') {
return res.status(400).json({ error: '构建结果必须为 done、failed 或 done_no_uidone_no_ui=构建成功但确认无前端界面)' });
}
// §2.2 / §2.7.1 hasWeb 校验:构建完成且判定有 Web 形态 service_url 必填(严格拦截)
if (buildStatus === 'done') {
// a_done 时保留 clone 目录,供确定性探测判定运行形态
// hasWeb 校验(2026-08-27 放宽):Web 形态未填 service_url 时不硬拦截,降级放行
// B 阶段内部"判定为 Web 形态但未提供服务地址,跳过浏览器测试"),避免批量评审卡死
if (buildStatus !== 'failed') {
const cloneDir = path.resolve(__dirname, '../../data/clone', eid(req));
const webMode = resolveWebMode(eid(req), fs.existsSync(cloneDir) ? cloneDir : undefined);
if (webMode.hasWeb && !(entry.service_url || '').trim()) {
return res.status(400).json({ error: '请先填写服务地址后再启动系统验证' });
const logs = (() => { try { return JSON.parse(entry.progress_log || '[]'); } catch { return []; } })();
logs.push({ time: new Date().toISOString(), status: 'verifying', msg: buildStatus === 'done_no_ui'
? '评委确认构建成功但无前端界面(done_no_ui),B 阶段触发实现完整度封顶'
: '判定为 Web 形态但未提供服务地址,跳过浏览器测试(降级放行,B 阶段继续代码评审)' });
db.prepare("UPDATE entries SET progress_log = json(?) WHERE id = ?").run(JSON.stringify(logs), eid(req));
}
}
startReviewB(eid(req), buildStatus);
res.json({ success: true });
});
// §2.5.1 浏览器验证恢复端点(2026-08-31):评审环境自动启动失败(awaiting_browse)后,
// 评委手动启动服务并填写 service_url,调用本端点恢复 B 阶段浏览器验证 + 评分。
router.post('/:entryId/verify-browse', (req: Request, res: Response) => {
if (!verifyProject(pid(req), res)) return;
const entry = db.prepare('SELECT * FROM entries WHERE id = ? AND project_id = ?').get(eid(req), pid(req)) as any;
if (!entry) return res.status(404).json({ error: '条目不存在' });
if (entry.status !== 'awaiting_browse') return res.status(409).json({ error: `当前状态(${entry.status})不在等待浏览器验证,无法恢复` });
// 评委手动启动服务后填写的访问地址(必填)
const serviceUrl = String(req.body?.service_url || '').trim();
if (!serviceUrl) return res.status(400).json({ error: '请填写手动启动后的服务访问地址(service_url' });
const logs = (() => { try { return JSON.parse(entry.progress_log || '[]'); } catch { return []; } })();
logs.push({ time: new Date().toISOString(), status: 'verifying', msg: `评委手动启动完成,服务地址: ${serviceUrl},恢复浏览器验证` });
db.prepare("UPDATE entries SET service_url = ?, progress_log = json(?), updated_at = datetime('now') WHERE id = ?").run(serviceUrl, JSON.stringify(logs), eid(req));
// 保持 awaiting_browse 状态交给 executeReviewB 处理(它识别该状态并设 trustedBrowse
// 若提前改成 verifying 会丢失"评委人工确认"标志,SSRF 会拦截 localhost 手动启动的服务)
startReviewB(eid(req), 'done');
res.json({ success: true });
});
router.post('/:entryId/cancel', (req: Request, res: Response) => {
if (!verifyProject(pid(req), res)) return;
const entry = db.prepare('SELECT * FROM entries WHERE id = ? AND project_id = ?').get(eid(req), pid(req)) as any;
@@ -629,6 +664,8 @@ router.get('/:entryId/report/export', async (req: Request, res: Response) => {
try {
const filePath = await generateEntryPdf(entry, project);
const filename = `${project.name}_${entry.title}_评审报告.pdf`.replace(/[<>:"/\\|?*]/g, '_');
// 2026-08-28 修复:强制 octet-stream 下载,避免浏览器将 PDF 打开预览而非保存到下载目录
res.setHeader('Content-Type', 'application/octet-stream');
res.download(filePath, filename);
} catch (err: any) {
res.status(500).json({ error: 'PDF生成失败: ' + err.message });
+6 -3
View File
@@ -38,10 +38,11 @@ function loadSnapshotScores(entryIds: string[]): Record<string, { score: number
return byEntry;
}
/** 计算展示分:聚合(正式,≥3 次)或单次最新(初评 <3 次)。聚合仅当标准一致且快照有 score */
/** 计算展示分:聚合(正式,≥NN=config.aggregateRounds)或单次最新(初评 <N 次)。聚合仅当标准一致且快照有 score */
function displayScoreFor(e: any, snapshots: { score: number | null; standard_snapshot: string | null }[]) {
const agg = aggregateEntryScores(snapshots, 3);
if (agg && agg.count >= 3) {
const ROUNDS = config.aggregateRounds;
const agg = aggregateEntryScores(snapshots, ROUNDS);
if (agg && agg.count >= ROUNDS) {
return { score: agg.value, aggregate_count: agg.count, is_formal: true };
}
return { score: e.final_score || e.raw_score, aggregate_count: agg ? agg.count : 0, is_formal: false };
@@ -230,6 +231,8 @@ router.get('/:id/summary/export', async (req: Request, res: Response) => {
try {
const filePath = await generateSummaryPdf(project, buildSummary(id));
const filename = `${project.name}_汇总排名.pdf`.replace(/[<>:"/\\|?*]/g, '_');
// 2026-08-28 修复:强制 octet-stream 下载,避免浏览器将 PDF 打开预览而非保存到下载目录
res.setHeader('Content-Type', 'application/octet-stream');
res.download(filePath, filename);
} catch (err: any) {
res.status(500).json({ error: 'PDF生成失败: ' + err.message });
+85
View File
@@ -0,0 +1,85 @@
import fs from 'fs';
import path from 'path';
import { exec } from 'child_process';
/**
* 验收命令执行器(2026-08-26 P2):解析 README 的 `## 验收` 命令块并真实执行,
* 留存输出作为功能完整性/实现完整度的确定性证据。
*
* README 约定格式:
* ```
* ## 验收
* npm run accept -- --input data/sample.xlsx
* # 预期:输出统计看板并生成 report.html
* ```
*/
export interface AcceptResult {
found: boolean;
command: string | null;
ok: boolean;
output: string;
error: string | null;
durationMs: number;
}
const ACCEPT_MARKER = /^##\s*验收/;
const ACCEPT_CMD_RE = /^(npm|npx|python|python3|node|bash|sh|\.\/|uv)\s+[\s\S]+$/;
function findAcceptBlock(readme: string): { cmd: string; expect: string } | null {
const lines = readme.split('\n');
for (let i = 0; i < lines.length; i++) {
if (ACCEPT_MARKER.test(lines[i])) {
// 命令通常是标题下一行非注释内容
const cmdLines: string[] = [];
for (let j = i + 1; j < lines.length; j++) {
const l = lines[j].trim();
if (!l) continue;
if (l.startsWith('#') || l.startsWith('<!--')) continue;
if (ACCEPT_MARKER.test(l) && j > i) break;
if (ACCEPT_CMD_RE.test(l)) { cmdLines.push(l); break; }
if (cmdLines.length === 0 && !/^##/.test(l)) { cmdLines.push(l); break; }
}
if (cmdLines.length > 0) return { cmd: cmdLines[0], expect: '' };
}
}
return null;
}
export function runAccept(dir: string, timeoutMs = 90000, dryRun = false): Promise<AcceptResult> {
return new Promise((resolve) => {
let readme = '';
try { readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8'); } catch { readme = ''; }
if (!readme.trim()) {
return resolve({ found: false, command: null, ok: false, output: '', error: 'README 不存在', durationMs: 0 });
}
const block = findAcceptBlock(readme);
if (!block) {
return resolve({ found: false, command: null, ok: false, output: '', error: 'README 未定义 ## 验收 命令块', durationMs: 0 });
}
if (dryRun) {
// 测试/CI 环境不真实执行命令,仅标记"检测到验收块"
return resolve({ found: true, command: block.cmd, ok: true, output: '[dry-run] 未执行', error: null, durationMs: 0 });
}
const t0 = Date.now();
const child = exec(block.cmd, { cwd: dir, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
const output = `${stdout || ''}\n${stderr || ''}`.trim().slice(0, 4000);
resolve({
found: true, command: block.cmd, ok: !err, output,
error: err ? (err.message || '').slice(0, 500) : null,
durationMs: Date.now() - t0,
});
});
if (timeoutMs) {
setTimeout(() => { try { child.kill(); } catch { } }, timeoutMs + 1000);
}
});
}
export function renderAcceptToPrompt(r: AcceptResult): string {
if (!r.found) {
return '\n## 验收命令确定性检查\n⚠️ README 未定义 `## 验收` 命令块。规范要求提供一键验收命令证明核心闭环可运行,此情况应在功能/实现维度体现。';
}
const status = r.ok ? '✅ 执行成功' : '❌ 执行失败';
return `\n## 验收命令确定性检查(系统真实执行)\n- 命令:\`${r.command}\`\n- 状态:${status}(耗时 ${(r.durationMs / 1000).toFixed(1)}s\n- 输出片段:${(r.output || '').slice(0, 500) || '(无输出)'}`;
}
+250
View File
@@ -0,0 +1,250 @@
import { callDeepSeek } from './deepseek';
/**
* 文档声称-代码实现一致性校验(2026-08-27)。
*
* 目的:防"拿到评审报告后针对性补齐文档/声称,欺骗评审系统"。
* 思路:不是对比"这次 vs 上次",而是校验"文档声称的功能 ↔ 代码真实实现"的一致性——
* 参赛者补齐真实文档(声称的功能代码里有)→ 一致性高 → 合理加分;
* 补齐编造的声称(代码里没有/占位)→ 一致性率低 → 判定虚报,文档类维度封顶。
*/
export interface ClaimItem {
feature: string;
source: string; // 来源文档路径
}
export interface ClaimConsistencyResult {
claimsCount: number;
matchedCount: number;
consistencyRatio: number; // 0~1
fakeClaims: string[]; // 声称但代码未真实实现的功能
reliable: boolean; // ratio >= 0.9
partiallyReliable: boolean; // 0.6 <= ratio < 0.9
text: string; // 注入 prompt 的文本
}
const CONSISTENT_RELIABLE = 0.9;
const CONSISTENT_PARTIAL = 0.6;
/**
* AI 从 MD 文档(README/设计文档/AGENTS 等)提取声称的功能点清单。
* 只提取"声称",不做真实性判定(判定交给程序比对)。失败返回 []。
*/
export async function extractClaimsFromDocs(
mdFiles: { path: string; content: string }[]
): Promise<ClaimItem[]> {
try {
if (!mdFiles || mdFiles.length === 0) return [];
// 文档内容可能很大,截取前若干字符(README 通常能覆盖核心声称)
const docsText = mdFiles
.map(f => `--- ${f.path} ---\n${f.content.slice(0, 6000)}`)
.join('\n\n')
.slice(0, 20000);
const prompt = `你是文档审计助手。以下是一个项目的说明文档(README/设计文档等)。
提取文档中【声称实现的】所有功能点,输出严格JSON数组:
[{"feature":"一句话功能描述","source":"来源文件路径"}]
- 只提取文档明确声称实现的功能,不提取愿景/计划/未来规划
- 每条 feature 用一句话描述(尽量具体,如"支持PDF文档解析"而非"文档处理"
文档:
${docsText}`;
const raw = await callDeepSeek(prompt, 1, 'claim-extract');
if (!raw) return [];
const arr = JSON.parse(raw.replace(/```(?:json)?\s*([\s\S]*?)```/g, '$1').trim());
if (!Array.isArray(arr)) return [];
return arr
.filter((c: any) => c && typeof c.feature === 'string' && c.feature.trim())
.map((c: any) => ({ feature: c.feature.trim(), source: String(c.source || '') }));
} catch {
return [];
}
}
/**
* 中文功能名相似度(子串/包含/编辑距离近似)。用于模糊匹配声称功能与代码真实功能。
* 简化:任一包含关系或公共子串长度 ≥ 4 即视为匹配(中文功能描述场景下足够)。
*/
function featureMatch(claim: string, real: string): boolean {
const a = claim.toLowerCase().replace(/\s+/g, '');
const b = real.toLowerCase().replace(/\s+/g, '');
if (a === b) return true;
if (a.length > 2 && (a.includes(b) || b.includes(a))) return true;
// 公共子串 ≥ 5 字符(中文功能描述通常有关键词重叠,如"PDF解析"/"文档解析"
for (let len = Math.min(a.length, b.length, 5); len >= 4; len--) {
for (let i = 0; i + len <= a.length; i++) {
const sub = a.slice(i, i + len);
if (b.includes(sub)) return true;
}
}
return false;
}
/**
* 程序化比对:声称功能清单 ↔ 功能发现轮(代码真实实现清单)。
* - 声称项在 inventory 中 depth=real 且特征匹配 → 一致
* - 声称项 depth=partial → 部分实现(算 0.5
* - 声称项 depth=stub 或不存在 → 虚报
*/
export function computeClaimConsistency(
claims: ClaimItem[],
inventory: { feature: string; depth?: string }[]
): ClaimConsistencyResult | null {
if (!claims || claims.length === 0) return null;
if (!inventory || inventory.length === 0) {
return {
claimsCount: claims.length,
matchedCount: 0,
consistencyRatio: 0,
fakeClaims: claims.map(c => c.feature),
reliable: false,
partiallyReliable: false,
text: '',
};
}
const real = inventory.filter(f => f.depth === 'real');
const partial = inventory.filter(f => f.depth === 'partial');
const fakeClaims: string[] = [];
let matched = 0;
for (const c of claims) {
if (real.some(f => featureMatch(c.feature, f.feature))) {
matched += 1;
} else if (partial.some(f => featureMatch(c.feature, f.feature))) {
matched += 0.5;
} else {
fakeClaims.push(c.feature);
}
}
const ratio = Math.min(1, matched / claims.length);
return {
claimsCount: claims.length,
matchedCount: Math.round(matched),
consistencyRatio: ratio,
fakeClaims,
reliable: ratio >= CONSISTENT_RELIABLE,
partiallyReliable: ratio >= CONSISTENT_PARTIAL && ratio < CONSISTENT_RELIABLE,
text: '',
};
}
/**
* AI 语义比对(2026-08-27 修正):把声称清单 + 代码真实实现清单一起给 LLM,
* 由 AI 逐项判断"声称功能是否在代码实现中真实存在"——解决纯字符串匹配把
* 语义相同但措辞不同的功能误判为虚报的问题(如"InferenceEngine" vs "调用推理引擎chat_structured")。
* 返回每项声称的判定:real/partial/stub。失败 → null(调用方回退纯函数匹配)。
*
* 运行验证增强(2026-08-31):注入运行时证据(构建/测试/前端访问结果),
* 让 AI 识别"代码里有但跑不起来"的声称(动态失效)——三级真实性校验:
* 文档声称 ↔ 代码存在 ↔ 运行验证通过。
*/
export async function verifyClaimsByAI(
claims: ClaimItem[],
inventory: { feature: string; depth?: string }[],
runtime?: {
buildOk?: boolean;
buildSummary?: string;
testsPassed?: boolean;
testSummary?: string;
webAccessible?: boolean;
webSummary?: string;
testPassedCount?: number;
testRunCount?: number;
}
): Promise<{ feature: string; verdict: 'real' | 'partial' | 'stub' }[] | null> {
try {
if (!claims || claims.length === 0) return [];
if (!inventory || inventory.length === 0) return claims.map(c => ({ feature: c.feature, verdict: 'stub' as const }));
const claimsText = claims.map(c => `${c.feature}(来源:${c.source}`).join('\n');
const invText = inventory.map(f => `- ${f.feature} [${f.depth}]`).join('\n');
// 运行验证证据(2026-08-31 新增)
const runtimeLines: string[] = [];
if (runtime) {
if (runtime.buildOk !== undefined) runtimeLines.push(`- 构建:${runtime.buildOk ? '成功' : '失败/未通过'}${runtime.buildSummary ? '' + runtime.buildSummary.slice(0, 120) + '' : ''}`);
if (runtime.testsPassed !== undefined) {
const tc = runtime.testPassedCount != null && runtime.testRunCount != null
? `${runtime.testPassedCount}/${runtime.testRunCount} 用例通过)`
: '';
runtimeLines.push(`- 测试:${runtime.testsPassed ? '通过' : '失败/无法运行'}${tc}${runtime.testSummary ? '' + runtime.testSummary.slice(0, 120) + '' : ''}`);
}
if (runtime.webAccessible !== undefined) runtimeLines.push(`- 前端/Web服务:${runtime.webAccessible ? '可访问' : '无法访问'}${runtime.webSummary ? '' + runtime.webSummary.slice(0, 120) + '' : ''}`);
}
const runtimeText = runtimeLines.length > 0
? `\n\n## 运行验证结果(确定性证据,系统真实执行)\n${runtimeLines.join('\n')}\n判定规则:文档声称的功能若依赖可运行系统(Web服务/构建产物/测试通过),而对应运行验证失败,则该声称最多判定为 partial(存在但不可用),不得判定 real。`
: '';
const prompt = `你是代码审计助手。以下是文档声称实现的功能清单,以及代码审计(功能发现轮)识别出的代码真实功能清单。
请逐项判断每个【文档声称功能】在【代码真实功能清单】中是否有对应实现(语义等价即可,措辞不必相同)。${runtimeText}
文档声称功能:
${claimsText}
代码真实功能清单:
${invText}
输出严格JSON数组,每项对应一条声称(顺序一致):
[{"feature":"声称功能原文","verdict":"real|partial|stub","evidence":"对应代码功能原文(无则空)"}]
- real=代码中有语义等价的真实实现,且(若依赖运行验证)运行验证通过
- partial=代码中有部分实现(主干在但边界缺失),或代码存在但运行验证失败(存在但不可用)
- stub=代码中无对应实现或仅占位
- 判断要宽松:语义等价即算 real,不要因措辞不同误判;但运行验证失败的功能不得判 real`;
const raw = await callDeepSeek(prompt, 1, 'claim-verify');
if (!raw) return null;
const arr = JSON.parse(raw.replace(/```(?:json)?\s*([\s\S]*?)```/g, '$1').trim());
if (!Array.isArray(arr)) return null;
const result = claims.map((c, i) => {
const v = arr[i];
const verdict = v && (v.verdict === 'real' || v.verdict === 'partial' || v.verdict === 'stub') ? v.verdict : 'stub';
return { feature: c.feature, verdict };
});
return result;
} catch {
return null;
}
}
/**
* 由 AI 判定结果生成一致性结果(AI 语义比对优先,fallback 纯函数匹配)。
*/
export function buildConsistencyFromVerdicts(
claims: ClaimItem[],
verdicts: { feature: string; verdict: 'real' | 'partial' | 'stub' }[]
): ClaimConsistencyResult {
const fakeClaims: string[] = [];
let matched = 0;
for (const c of claims) {
const v = verdicts.find(x => x.feature === c.feature);
if (!v || v.verdict === 'stub') {
fakeClaims.push(c.feature);
} else if (v.verdict === 'real') {
matched += 1;
} else {
matched += 0.5;
}
}
const ratio = Math.min(1, claims.length > 0 ? matched / claims.length : 0);
return {
claimsCount: claims.length,
matchedCount: Math.round(matched),
consistencyRatio: ratio,
fakeClaims,
reliable: ratio >= CONSISTENT_RELIABLE,
partiallyReliable: ratio >= CONSISTENT_PARTIAL && ratio < CONSISTENT_RELIABLE,
text: '',
};
}
/**
* 生成注入 prompt 的一致性文本(确定性证据 + 评分绑定规则)。
*/
export function renderClaimConsistencyText(r: ClaimConsistencyResult): string {
const pct = Math.round(r.consistencyRatio * 100);
const bound = r.reliable
? '文档声称与代码实现高度一致(≥90%),文档可信,文档类维度正常给分'
: r.partiallyReliable
? '文档声称与代码实现部分一致(60~90%),文档基本可信,文档类维度按 80% 权重'
: '文档声称与代码实现严重不符(<60%),判定文档虚报:声称的功能在代码中未真实实现,文档类维度封顶至满分 50%,并核实以下虚报项';
const fakeNote = r.fakeClaims.length > 0
? `\n虚报功能清单(声称但代码未真实实现):\n${r.fakeClaims.slice(0, 15).map(f => `- ${f}`).join('\n')}`
: '';
return `\n## 文档声称-代码实现一致性(确定性证据,评分必须据此)\n` +
`系统提取文档声称功能 ${r.claimsCount} 项,与代码真实实现(功能发现轮)比对:一致 ${r.matchedCount} 项,一致性率 ${pct}%。\n` +
`判定:${bound}${fakeNote}\n`;
}
+170
View File
@@ -0,0 +1,170 @@
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'),
};
}
+45 -1
View File
@@ -1,4 +1,4 @@
import { REVIEW_CONSTANTS, isBuildRelatedDim } from './review-constants';
import { REVIEW_CONSTANTS, isBuildRelatedDim, isTruthBoundDim } from './review-constants';
export interface HardRuleContext {
buildFailed: boolean;
@@ -6,6 +6,15 @@ export interface HardRuleContext {
duplicateRatio: number;
hasAnyReadme: boolean;
hasRootReadme: boolean;
// 文档声称-代码实现一致性率(2026-08-27):<0.6 → 文档类维度封顶 50%(防假文档/补声称刷分)
claimConsistencyRatio?: number;
// 代码质量(2026-08-27):空壳率/测试有效度 → 规模功能点降档(防数量虚胖)
stubRatio?: number;
testValidityRatio?: number;
// 真实前端缺失(2026-08-31):web 形态但无真实前端界面(或前端存在但不可访问)
// → 实现完整度/稳定性封顶 50%。CLI/VS Code 插件豁免(frontendCliOrExtension=true 时不触发)。
frontendAbsent?: boolean;
frontendCliOrExtension?: boolean;
}
export interface HardRuleDim {
@@ -22,6 +31,12 @@ export interface HardRuleResult {
/**
* 评审 Phase 3c:确定性硬规则封顶(校准后执行)。
* 纯函数、不修改入参;封顶只降不升。
*
* 真实性绑定(2026-08-27):构建失败时——
* - 功能类维度(实现完整度/稳定性等):封顶 33%
* - 纸面维度(场景价值/架构/工具/文档/规范/安全/AI日志等):封顶 33%,
* 防止"系统跑不起来但假文档拿高分"
* - 效果与数据:封顶 30%(效果真实性依赖可运行系统)
*/
export function applyHardRules(
dims: HardRuleDim[],
@@ -33,11 +48,40 @@ export function applyHardRules(
let cap = d.maxScore;
const isBuildCapDim = isBuildRelatedDim(name);
if (ctx.buildFailed && isBuildCapDim) cap = Math.min(cap, Math.floor(d.maxScore * REVIEW_CONSTANTS.BUILD_FAIL_CAP_RATIO));
// 纸面维度真实性绑定:构建失败 → 半封顶(防假文档高分)
if (ctx.buildFailed && isTruthBoundDim(name) && !isBuildCapDim) cap = Math.min(cap, Math.floor(d.maxScore * REVIEW_CONSTANTS.TRUTH_BIND_CAP_RATIO));
if (ctx.buildFailed && name.includes('效果与数据')) cap = Math.min(cap, Math.floor(d.maxScore * REVIEW_CONSTANTS.EFFECT_DATA_BUILD_FAIL_RATIO));
if (ctx.testStepFailed && (name.includes('效果与数据') || isBuildCapDim)) cap = Math.min(cap, Math.floor(d.maxScore * REVIEW_CONSTANTS.TEST_FAIL_CAP_RATIO));
// 文档声称-代码实现一致性(2026-08-27):一致性<60% → 文档类维度封顶 50%(文档虚报)
if (typeof ctx.claimConsistencyRatio === 'number' && ctx.claimConsistencyRatio < 0.6 && name.includes('演示与文档')) {
cap = Math.min(cap, Math.floor(d.maxScore * 0.5));
}
// 代码质量(2026-08-27):空壳率>25% → 规模功能点降档 25%(数量虚胖,质量差)
if (typeof ctx.stubRatio === 'number' && ctx.stubRatio > 0.25 && name.includes('规模')) {
cap = Math.min(cap, Math.floor(d.maxScore * 0.75));
}
// 代码质量:测试有效度低(<0.4)→ 规模功能点降档 25%(测试只测边角料,不支撑"覆盖完整")
if (typeof ctx.testValidityRatio === 'number' && ctx.testValidityRatio < 0.4 && ctx.testValidityRatio > 0 && name.includes('规模')) {
cap = Math.min(cap, Math.floor(d.maxScore * 0.75));
}
if (ctx.duplicateRatio > REVIEW_CONSTANTS.DUP_RATIO_CAP_TRIGGER && name.includes('代码规范')) cap = Math.min(cap, REVIEW_CONSTANTS.DUP_CAP_SCORE);
if (!ctx.hasAnyReadme && name.includes('演示与文档')) cap = Math.min(cap, REVIEW_CONSTANTS.NO_README_CAP);
else if (!ctx.hasRootReadme && name.includes('演示与文档')) cap = Math.min(cap, REVIEW_CONSTANTS.NO_ROOT_README_CAP);
// 真实前端缺失(2026-08-31):web 形态但无真实前端界面 → 实现完整度/稳定性 ≤50%。
// 不触发条件:CLI 工具 / VS Code 插件(其界面=IDE 集成/命令行交互)。效果与数据不在此封顶
// (效果数据由可验证三档单独管理;构建成功但无前端的后端项目仍可能有真实效果)。
if (ctx.frontendAbsent && !ctx.frontendCliOrExtension && isBuildCapDim) {
cap = Math.min(cap, Math.floor(d.maxScore * 0.5));
}
// 运行验证全面失败(2026-08-31):前端缺失/不可访问 且 测试失败 → 作品未达到"可运行交付",
// 静态纸面维度(场景价值/架构/工具/代码规范/AI日志等)封顶 50%。
// 目的:防止"代码规模大、文档好但只是原型/跑不起来"的项目靠静态维度拿高分——
// 真实区分度应在"能否构建/启动/通过测试",而非代码体量与文档包装。
// 规模与功能点也属静态维度(原型可堆代码量),一并封顶。
const isStaticDim = isTruthBoundDim(name) || name.includes('规模');
if (ctx.frontendAbsent && !ctx.frontendCliOrExtension && ctx.testStepFailed && isStaticDim && !isBuildCapDim) {
cap = Math.min(cap, Math.floor(d.maxScore * 0.5));
}
let score = d.score;
if (score > cap) {
log.push(`${name} ${score}${cap}(超上限${cap}`);
+78
View File
@@ -0,0 +1,78 @@
import fs from 'fs';
import path from 'path';
/**
* 测量协议结构化检查(2026-08-26 P2):
* 校验"提效/效果"声称是否有结构化测量协议支撑——data/measurement/ 齐备性 +
* 原始记录可复现。结果注入提效幅度/效果类维度 prompt。
*
* 要求(对齐规范 §6.3 效果总结):
* - data/measurement/ 存在
* - baseline.md 或基线路由(记录"无工具时怎么测")
* - timing-log 或原始记录文件(.csv/.json/.log,含时间戳)
* - 可重演命令(README 验收块或 measurement/run.sh
*/
export interface MeasurementAudit {
dirExists: boolean;
hasBaseline: boolean;
hasTimingLog: boolean;
hasRerunScript: boolean;
files: string[];
toPrompt: string;
}
const BASELINE_RE = /baseline|base|对照|人工/;
const TIMING_RE = /timing|log|record|耗时|记录|measure|measurement|result|data/;
const RERUN_RE = /run\.|accept|verify|measure|\.sh$|\.py$|\.js$|package\.json/;
export function auditMeasurement(dir: string): MeasurementAudit {
const result: MeasurementAudit = {
dirExists: false, hasBaseline: false, hasTimingLog: false, hasRerunScript: false,
files: [], toPrompt: '',
};
const mDir = path.join(dir, 'data', 'measurement');
try {
if (fs.existsSync(mDir) && fs.statSync(mDir).isDirectory()) {
result.dirExists = true;
result.files = fs.readdirSync(mDir);
for (const f of result.files) {
const lower = f.toLowerCase();
if (BASELINE_RE.test(lower) && !result.hasBaseline) result.hasBaseline = true;
if (TIMING_RE.test(lower) && !result.hasTimingLog) result.hasTimingLog = true;
if (RERUN_RE.test(lower) && !result.hasRerunScript) result.hasRerunScript = true;
}
}
} catch { /* 目录不存在则视为缺失 */ }
// 降级:若 data/measurement 不存在,检查 data/ 下是否有测量相关文件
if (!result.dirExists) {
try {
const dataDir = path.join(dir, 'data');
if (fs.existsSync(dataDir)) {
const dataFiles = fs.readdirSync(dataDir);
for (const f of dataFiles) {
if (TIMING_RE.test(f.toLowerCase()) && !result.hasTimingLog) result.hasTimingLog = true;
}
}
} catch { }
}
const lines = ['', '## 测量协议确定性检查(系统自动检测,提效/效果类维度评分必须参考)'];
if (result.dirExists) {
lines.push(`- data/measurement/ 存在,文件:${result.files.join(', ') || '(空)'}`);
} else {
lines.push('- data/measurement/ 目录缺失');
}
lines.push(`- 基线描述(baseline.md):${result.hasBaseline ? '✅ 有' : '❌ 无'}`);
lines.push(`- 原始计时/记录(timing-log.csv 等):${result.hasTimingLog ? '✅ 有' : '❌ 无'}`);
lines.push(`- 可重演命令:${result.hasRerunScript ? '✅ 有' : '❌ 无'}`);
lines.push('');
if (!result.dirExists || !result.hasBaseline || !result.hasTimingLog) {
lines.push('**评分约束**:提效/效果类维度若无测量协议支撑,不得给出"数据充分可信"的评价;数字应按未证实处理。');
} else {
lines.push('**评分提示**:测量协议齐全,若 README 数字与原始记录可对账,可视为有据可查。');
}
result.toPrompt = '\n' + lines.join('\n');
return result;
}
+23
View File
@@ -10,6 +10,9 @@ export const REVIEW_CONSTANTS = {
TEST_FAIL_CAP_RATIO: 0.5,
EFFECT_DATA_BUILD_FAIL_RATIO: 0.3,
DUP_RATIO_CAP_TRIGGER: 0.5,
// 真实性绑定(2026-08-27):构建失败时,文档/设计/规范等"纸面维度"半封顶,
// 防止假文档在系统跑不起来时仍拿高分
TRUTH_BIND_CAP_RATIO: 0.33,
L2_PASS_RATIO: 0.6,
L3_RATIO: 0.8,
DEFAULT_LATE_PENALTY: 5,
@@ -20,6 +23,26 @@ export const REVIEW_CONSTANTS = {
CAL_UNSTABLE_WEIGHT: 0.8,
} as const;
// 构建失败需封顶的"纸面维度"2026-08-27):文档/设计/规范类,系统跑不起来则这些维度一律降权
// 覆盖:场景价值、开发范式、架构设计、工具使用、演示与文档、代码规范、安全性、AI使用日志
const TRUTH_BIND_KEYWORDS = [
'场景价值',
'开发范式',
'架构设计',
'架构',
'工具使用',
'演示与文档',
'代码规范',
'代码规范性',
'安全性',
'AI使用日志',
'AI日志',
];
export function isTruthBoundDim(name: string): boolean {
return TRUTH_BIND_KEYWORDS.some(k => name.includes(k));
}
export const BUILD_RELATED_KEYWORDS = [
'实现完整度与稳定性',
'实现完整度',
+576 -50
View File
@@ -11,10 +11,15 @@ import { matchDimKey, computeLatePenalty, computeCalibration, parseDimResponse,
import { isPathInside } from '../path-security';
import { applyHardRules, applyTrackHardRules } from './hard-rules';
import { findL2Topic, buildL2FuncContext } from './l2-topics';
import { loadTeams } from './teams-config';
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 { auditMeasurement } from './measurement-audit';
import { runAccept, renderAcceptToPrompt } from './accept-runner';
import { extractClaimsFromDocs, computeClaimConsistency, verifyClaimsByAI, buildConsistencyFromVerdicts, renderClaimConsistencyText } from './claim-consistency';
import { computeCodeQuality } from './code-quality';
import {
REVIEW_CONSTANTS,
BUILD_SYSTEMS,
@@ -42,7 +47,7 @@ function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
}
let activeCount = 0;
const queue: { entryId: string; stage: 'A' | 'B'; buildStatus?: 'done' | 'failed' }[] = [];
const queue: { entryId: string; stage: 'A' | 'B'; buildStatus?: 'done' | 'failed' | 'done_no_ui' }[] = [];
export function startReview(entryId: string) {
const entry = db.prepare('SELECT * FROM entries WHERE id = ?').get(entryId) as any;
@@ -82,18 +87,23 @@ function maybeRunNextRound(entryId: string) {
}
// B 阶段启动??verify 触发):复???queue 机制,受 MAX_CONCURRENT 并发限制
export function startReviewB(entryId: string, buildStatus: 'done' | 'failed' = 'done') {
export function startReviewB(entryId: string, buildStatus: 'done' | 'failed' | 'done_no_ui' = 'done') {
const entry = db.prepare('SELECT * FROM entries WHERE id = ?').get(entryId) as any;
if (!entry || entry.status !== 'a_done') return;
// 2026-08-31:允许从 awaiting_browse 恢复(评委手动启动后填 service_url
if (!entry || (entry.status !== 'a_done' && entry.status !== 'awaiting_browse')) return;
const active = db.prepare("SELECT COUNT(*) as cnt FROM entries WHERE status IN ('queued','cloning','analyzing','verifying')").get() as any;
if (active.cnt >= MAX_CONCURRENT) {
// 等待恢复时若并发满,保持 awaiting_browse(不进 verifying,避免丢失评委确认标志)
if (entry.status === 'awaiting_browse') { queue.push({ entryId, stage: 'B', buildStatus }); return; }
db.prepare("UPDATE entries SET status = 'verifying' WHERE id = ?").run(entryId);
queue.push({ entryId, stage: 'B', buildStatus });
return;
}
db.prepare("UPDATE entries SET status = 'verifying' WHERE id = ?").run(entryId);
// 从 awaiting_browse 恢复时保持该状态(executeReviewB 识别它并设 trustedBrowse);
// 正常 a_done → verifying
if (entry.status === 'a_done') db.prepare("UPDATE entries SET status = 'verifying' WHERE id = ?").run(entryId);
runReview(entryId, 'B', buildStatus);
}
@@ -148,9 +158,17 @@ async function maybeFinalizeL2Abandoned(entry: any, dir: string): Promise<boolea
* L2考核查重初筛:与 data/clone/ 下其他 clone 目录跨仓库比对(MD5 精确 + 归一化相似度),
* 并分析提交行为信号。确定性检测,flags 落库(plagiarism_json)供人工复核,不自动定罪。
* 仅在 L2考核 且存在其他 clone 目录时执行。
*
* 注意:技术大赛(赛道一/赛道二)不参与查重——技术大赛是开放创作,重复不是考核项,
* 查重仅服务 L2 考核的"独立完成"红线(2026-08-27 回退 P2 的全赛道扩展)。
*/
async function runPlagiarismScreening(entry: any, dir: string): Promise<PlagiarismReport | null> {
if (getProjectTrack(entry.project_id) !== 'L2考核') return null;
// 仅 L2考核 执行查重;技术大赛跳过(开放创作,查重无价值)
const track = entry.category_tag || entry.project_track || '';
if (track !== 'L2考核') {
pipeLog(entry.id, 'PLAG', `skipped (track=${track || '?'} 非 L2考核)`);
return null;
}
const tPlag = Date.now();
try {
const targetFiles = readRepoFiles(dir);
@@ -220,7 +238,10 @@ function buildAuditExtraContexts(
entryId: string,
standardDims: { name: string }[],
files: { path: string; content?: string; size?: number }[],
featureInvText: string
featureInvText: string,
effectAuditPrompt = '',
claimConsistencyText = '',
qualityText = ''
): Record<string, string> {
const ctx: Record<string, string> = {};
@@ -243,17 +264,27 @@ function buildAuditExtraContexts(
// 4. 组装:定向追踪模板注入实现类维度;日志审计注入 AI 日志维度;符号索引注入所有实现类维度
const IMPL_RE = /功能完整|实现完整|规模|功能点/;
const LOG_RE = /AI.*日志|AI协作/;
const EFFECT_RE = /提效|效果|效率|数据/;
// 文档真实性(2026-08-27):声称一致性注入文档/场景/价值类维度
const DOC_RE = /演示与文档|场景价值|技术合理|价值/;
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);
if (qualityText) texts.push(qualityText);
texts.push(getDirectedTraceTemplate());
}
if (LOG_RE.test(dim.name) && logAuditPrompt) {
texts.push(logAuditPrompt);
}
if (EFFECT_RE.test(dim.name) && effectAuditPrompt) {
texts.push(effectAuditPrompt);
}
if (DOC_RE.test(dim.name) && claimConsistencyText) {
texts.push(claimConsistencyText);
}
if (texts.length > 0) ctx[dim.name] = '\n' + texts.join('\n');
}
return ctx;
@@ -262,9 +293,24 @@ function buildAuditExtraContexts(
/**
* Map-Reduce 功能发现轮(2026-08-26 P1):对全部源码分块扫描,汇总实际功能清单,
* 注入实现类维度让 AI 对照 README/验收基准逐项核对。失败非致命(返回空串)。
* 整体看门狗(2026-08-28):串行 12 次 LLM 调用在 API 慢时可能拖 40+ 分钟卡死管线,
* 加整体超时(FEATURE_DISCOVERY_WATCHDOG_MS=240s),超时跳过功能发现轮(返回空串),不阻塞评审。
*/
const FEATURE_DISCOVERY_WATCHDOG_MS = 240000;
async function discoverFeatureInventory(entryId: string, files: { path: string; content?: string; size?: number }[]): Promise<string> {
try {
const result = await withTimeout(discoverFeatureInventoryInner(entryId, files), FEATURE_DISCOVERY_WATCHDOG_MS);
return result;
} catch (e: any) {
pipeLog(entryId, 'FEATURES', `watchdog-timeout (${FEATURE_DISCOVERY_WATCHDOG_MS / 1000}s) skipped: ${e.message}`);
return '';
}
}
async function discoverFeatureInventoryInner(entryId: string, files: { path: string; content?: string; size?: number }[]): Promise<string> {
try {
// 无 key(测试/降级环境)跳过,避免空转
if (!config.deepseekApiKey) return '';
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 '';
@@ -309,8 +355,15 @@ ${perChunk.map((p, i) => `--- 块${i + 1} ---\n${p}`).join('\n\n')}`;
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`);
// 落库(供详情页展示与跨快照复用),失败不影响注入
try {
db.prepare("UPDATE entries SET feature_inventory = ? WHERE id = ?").run(JSON.stringify(arr), entryId);
} catch (e: any) {
pipeLog(entryId, 'FEATURES', `store failed: ${e.message}`);
}
const statsBlock = buildFeatureStatsText(arr);
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');
return statsBlock + '\n## 功能发现明细(前 80 项,供逐项核对)\n' + lines.join('\n');
} catch {
return '';
}
@@ -320,9 +373,123 @@ ${perChunk.map((p, i) => `--- 块${i + 1} ---\n${p}`).join('\n\n')}`;
}
}
/**
* 文档声称-代码实现一致性校验(2026-08-27):
* AI 从 MD 文档提取声称功能清单 → 与功能发现轮 feature_inventory(代码真实实现)程序化比对
* → 一致性率 + 虚报清单。结果落库 entries.claim_consistency_json 供 B 阶段复用 + 硬规则。
* 非致命(无 md/无 key/失败 → 返回空串)。
*/
async function buildClaimConsistency(entryId: string, files: { path: string; content?: string }[], runtime?: {
buildOk?: boolean;
buildSummary?: string;
testsPassed?: boolean;
testSummary?: string;
webAccessible?: boolean;
webSummary?: string;
testPassedCount?: number;
testRunCount?: number;
}): Promise<string> {
try {
if (!config.deepseekApiKey) return '';
const mdFiles = (files || []).filter(f =>
/\.md$/i.test(f.path) && typeof f.content === 'string' &&
!/node_modules|dist|build|__pycache__/i.test(f.path) &&
!/(ai.?usage.?log|ai_log)/i.test(f.path)) as { path: string; content: string }[];
if (mdFiles.length === 0) return '';
const claims = await extractClaimsFromDocs(mdFiles);
if (!claims || claims.length === 0) return '';
// 从 DB 读已落库的 feature_inventory
const row = db.prepare('SELECT feature_inventory FROM entries WHERE id = ?').get(entryId) as any;
let inventory: any[] = [];
try { inventory = JSON.parse(row?.feature_inventory || '[]'); } catch { inventory = []; }
// 优先 AI 语义比对(解决纯字符串匹配误判"语义相同措辞不同"),失败回退纯函数
let result = null as any;
const verdicts = await verifyClaimsByAI(claims, inventory, runtime);
if (verdicts) {
result = buildConsistencyFromVerdicts(claims, verdicts);
pipeLog(entryId, 'CLAIM', `mode=ai verdicts=${verdicts.length}`);
} else {
result = computeClaimConsistency(claims, inventory);
pipeLog(entryId, 'CLAIM', `mode=string-fallback`);
}
if (!result) return '';
result.text = renderClaimConsistencyText(result);
// 落库供 B 阶段/硬规则复用
try {
db.prepare("UPDATE entries SET claim_consistency_json = ? WHERE id = ?").run(JSON.stringify(result), entryId);
} catch (e: any) {
pipeLog(entryId, 'CLAIM', `store failed: ${e.message}`);
}
pipeLog(entryId, 'CLAIM', `claims=${result.claimsCount} matched=${result.matchedCount} ratio=${Math.round(result.consistencyRatio * 100)}% fake=${result.fakeClaims.length}`, 0);
return result.text;
} catch (e: any) {
pipeLog(entryId, 'CLAIM', `skipped: ${e.message}`);
return '';
}
}
/**
* 功能发现统计锚点文本(纯函数,可单测):从 feature_inventory 数组生成
* 确定性统计 + 评分绑定规则,供实现类维度注入。空/非法 → 空串。
*/
export function buildFeatureStatsText(arr: any[]): string {
if (!Array.isArray(arr) || arr.length === 0) return '';
const real = arr.filter((f: any) => f.depth === 'real').length;
const partial = arr.filter((f: any) => f.depth === 'partial').length;
const stub = arr.filter((f: any) => f.depth === 'stub').length;
const total = arr.length;
const realPct = Math.round((real / total) * 100);
return '\n## 全量代码功能发现统计(确定性证据,评分必须据此)\n' +
`系统 Map-Reduce 扫描全量源码,识别出 ${total} 个功能点(真实 ${real} / 部分 ${partial} / 占位 ${stub},真实占比 ${realPct}%)。\n` +
`评分绑定规则(本维度"功能点覆盖/实现完整度"子项必须据此):\n` +
`- 真实功能 ≥10 项 → 覆盖完整(满分档);5~9 项 → 中等;1~4 项 → 薄弱;0 项 → 0 分\n` +
`- 占位(stub) 比例 >30% → 覆盖子项降一档;>50% → 覆盖子项最高只给满分的一半\n` +
`- 功能发现清单仅作核对线索,统计数字是评分判据(防 AI 只看 README 声称、不看真实实现)\n`;
}
/**
* B 阶段复用 A 阶段已落库的功能发现统计(不重算 LLM):从 entries.feature_inventory
* 读取 JSON,重建确定性统计锚点文本,供实现类维度(实现完整度)评分。无库存量 → 返回空串。
*/
function renderStoredFeatureEvidence(entry: any): string {
try {
const raw = entry?.feature_inventory;
if (!raw) return '';
return buildFeatureStatsText(JSON.parse(raw));
} catch {
return '';
}
}
/**
* B 阶段复用 A 阶段已落库的文档声称一致性结果(不重算 LLM):从 entries.claim_consistency_json
* 读取,重建确定性文本注入文档/场景类维度。无结果 → 返回空串。
*/
function renderStoredClaimConsistency(entry: any): string {
try {
const raw = entry?.claim_consistency_json;
if (!raw) return '';
const r = JSON.parse(raw);
if (!r || typeof r.consistencyRatio !== 'number') return '';
return renderClaimConsistencyText(r);
} catch {
return '';
}
}
/** 从 entries.claim_consistency_json 读一致性率(供硬规则),无 → undefined */
function getClaimConsistencyRatio(entryId: string): number | undefined {
try {
const row = db.prepare('SELECT claim_consistency_json FROM entries WHERE id = ?').get(entryId) as any;
const r = JSON.parse(row?.claim_consistency_json || '');
return typeof r?.consistencyRatio === 'number' ? r.consistencyRatio : undefined;
} catch {
return undefined;
}
}
const DIRECTED_TRACE_TEMPLATE = `
## 定向追踪任务(本维度评分的核心依据)
请先从 README 中提取声称的核心功能清单,然后对每项功能执行以下四步:
1. **定位实现入口**(文件名:行号)
@@ -333,9 +500,8 @@ const DIRECTED_TRACE_TEMPLATE = `
- 占位=返回固定数据、空逻辑、仅UI无处理
4. **引用代码原文**作为证据(不少于1行)
输出严格JSON
{"checks":[{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub","evidence":"代码原文","reason":"判定理由"}],
"summary":"总体实现真实性结论"}
将每项核查结果填入主返回 JSON 的 "checks" 字段(见下方返回格式),每条为
{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub","evidence":"代码原文摘录","reason":"判定理由"}
此核对表直接决定本维度得分:
- 真实 = 该项满分权重
@@ -346,7 +512,7 @@ function getDirectedTraceTemplate(): string {
return '\n' + DIRECTED_TRACE_TEMPLATE;
}
async function runReview(entryId: string, stage: 'A' | 'B', buildStatus?: 'done' | 'failed') {
async function runReview(entryId: string, stage: 'A' | 'B', buildStatus?: 'done' | 'failed' | 'done_no_ui') {
activeCount++;
const tRun = Date.now();
try {
@@ -427,18 +593,43 @@ async function cloneRepo(entryId: string, repoUrl: string, branch: string, dir:
}
try {
const token = process.env.GITEA_TOKEN || config.giteaToken;
const username = process.env.GITEA_USERNAME || config.giteaUsername;
if (token && repoUrl.startsWith('https://')) {
const git = simpleGit();
// 认证(2026-08-31 增强):优先用该队伍自己的 Gitea tokenteams.json 每队独立 token),
// 全局评审账号 token 失效/无权限时也能拉取(实测全局账号对部分大仓库 401/525)。
// 按 repo_url 中的 ownergittea.user)匹配队伍 → 用队伍自己的 token。
let teamToken = '';
let teamUser = '';
try {
// repo_url 形如 https://gittea.dev/<owner>/<repo>.git 或 https://user@host/<owner>/<repo>.git
const url = new URL(repoUrl);
url.username = username || url.username;
url.password = token;
await withTimeout(git.clone(url.toString(), dir, cloneArgs), CLONE_TIMEOUT_MS);
} else {
await withTimeout(simpleGit().clone(repoUrl, dir, cloneArgs), CLONE_TIMEOUT_MS);
const owner = url.pathname.split('/').filter(Boolean)[0];
if (owner) {
const team = loadTeams().find((x: any) => x.gittea?.user === owner);
if (team?.gittea?.token) {
teamToken = team.gittea.token;
teamUser = team.gittea.user;
}
}
} catch { }
const token = teamToken || process.env.GITEA_TOKEN || config.giteaToken;
const username = teamUser || process.env.GITEA_USERNAME || config.giteaUsername;
// Gitea 间歇性 525 重试(2026-08-31):大仓库 clone 常遇 HTTP 525Cloudflare 层抖动),
// 单次失败即放弃会导致大仓库评审不稳定。重试 3 次,间隔 3s,均失败才报 clone_fail。
const cloneUrl = token && repoUrl.startsWith('https://')
? (() => { const u = new URL(repoUrl); u.username = username || u.username; u.password = token; return u.toString(); })()
: repoUrl;
let lastErr: any = null;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
if (attempt > 1) await new Promise(r => setTimeout(r, 3000 * attempt));
await withTimeout(simpleGit().clone(cloneUrl, dir, cloneArgs), CLONE_TIMEOUT_MS);
if (attempt > 1) addLog(entryId, 'cloning', `克隆重试第 ${attempt} 次成功`);
return true;
} catch (e: any) {
lastErr = e;
if (attempt < 3) addLog(entryId, 'cloning', `克隆失败(第 ${attempt}/3 次,将重试)`);
}
}
return true;
throw lastErr || new Error('clone failed');
} catch (err: any) {
const safeMsg = (err.message || '未知错误').replace(/https?:\/\/[^@\s]+@/g, 'https://***@');
addLog(entryId, 'clone_fail', '仓库克隆失败: ' + safeMsg);
@@ -619,7 +810,7 @@ interface BrowseResult {
summary: string;
}
async function tryBrowse(url: string, isWeb: boolean, dir: string, entryId = ''): Promise<BrowseResult> {
async function tryBrowse(url: string, isWeb: boolean, dir: string, entryId = '', trusted = false): Promise<BrowseResult> {
if (!isWeb) {
const cliCheck = fs.existsSync(path.join(dir, 'package.json')) ? 'node --version'
: fs.existsSync(path.join(dir, 'go.mod')) ? 'go version'
@@ -636,9 +827,13 @@ async function tryBrowse(url: string, isWeb: boolean, dir: string, entryId = '')
// 评审期二次校验(DNS 重绑定缓解):保存时校验过,但此时重新解析域名。
// 若解析结果已变为内网地址(重绑定攻击),拒绝导航并跳过浏览器测试。仅当开启 ssrfDnsCheck 时执行。
const ssrfResolve = await revalidateHost(url);
if (!ssrfResolve.ok) {
return { tested: true, pageLoaded: false, jsErrors: [], networkErrors: [], summary: `评审期 SSRF 二次校验拦截:${ssrfResolve.reason}` };
// trusted=true(评委手动确认的地址,2026-08-31):评委在 awaiting_browse 后手动启动服务填的地址
// 是人工确认过的管理操作,跳过 SSRF(否则 127.0.0.1 手动启动的服务会被拦截,人工启动方案失效)。
if (!trusted) {
const ssrfResolve = await revalidateHost(url);
if (!ssrfResolve.ok) {
return { tested: true, pageLoaded: false, jsErrors: [], networkErrors: [], summary: `评审期 SSRF 二次校验拦截:${ssrfResolve.reason}` };
}
}
try {
@@ -950,6 +1145,146 @@ function findIndexHtml(dir: string): boolean {
} catch { return false; }
}
// ================= 真实前端判定(2026-08-31=================
// 目的:区分「有真实前端界面」与「仅有后端服务/静态产物」。当前 detectWebMode 只要命中
// FastAPI/Flask 等即判定 web,但纯后端 API 项目(无任何前端页面)也会被判 web——导致
// AI 凭「服务可访问」给实现完整度/稳定性高分,即使项目根本没有用户界面。
// 判定原则(源码级,不做 DOM 渲染):
// - 真实前端:index.html(排除 coverage/dist 产物)、src/ 下 .tsx/.vue/.jsx/.jsx 组件、
// 前端构建配置(vite/webpack/next)、package.json 前端依赖(react/vue 等)
// - 静态产物:coverage/*.html、report.html 等非交互页面(不视为真实前端)
// - CLI/插件豁免:VS Code 插件(package.json contributes/ CLI 工具(bin)不要求 GUI
// 其界面 = IDE 集成/命令行交互,不算无前端
export interface FrontendDetect {
hasFrontend: boolean;
reason: string;
cliOrExtension: boolean;
}
export function detectFrontend(dir: string): FrontendDetect {
// CLI / VS Code 插件豁免判定(2026-08-31):
// - package.json binNode CLI)或 contributes/engines.vscodeVS Code 插件)
// - Go func main 无 http(纯命令行程序)
// - Python __main__/argparse/click/typer 且无 Web 框架
const cliOrExtension = (() => {
try {
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.bin) return true;
if (pkg.contributes || (pkg.engines && pkg.engines.vscode)) return true;
}
} catch { }
// 语言级 CLI 信号(无 package.json 的 Go/Python 等)
try {
const goFiles = scanSourceFiles(dir, ['.go'], 50, 5);
const goHead = goFiles.map(f => f.head).join('\n');
if (/func\s+main\s*\(/i.test(goHead) && !/(net\/http|http\.ListenAndServe|gin-gonic|labstack\/echo)/i.test(goHead)) return true;
} catch { }
try {
const pyFiles = scanSourceFiles(dir, ['.py'], 50, 5);
const pyHead = pyFiles.map(f => f.head).join('\n');
if (/(if\s+__name__\s*==\s*['\"]__main__['\"]|argparse|click\.command|import\s+typer)/i.test(pyHead) && !/(FastAPI|Flask\(|@app\.|streamlit|django)/i.test(pyHead)) return true;
} catch { }
return false;
})();
// 真实前端信号收集(2026-08-31):先收集前端信号,再决定豁免——
// web 形态且有真实前端时,不应被 CLI/插件豁免掩盖(否则有前端的项目误判为豁免)。
// CLI/插件豁免仅在「无任何真实前端信号」时生效(纯 CLI 工具/VS Code 插件不要求 GUI)。
const reasons: string[] = [];
// 0. Python Web 框架 + static/templates 中的 .html → 真实前端(FastAPI/Flask 等界面承载)
// 注意:仅凭 Web 框架不算有前端(纯 API 后端无页面),必须有 static/templates 下的 html 才算。
// 大项目 py 文件多(>100)时 scanSourceFiles 上限会漏掉入口文件,因此单独做不限量的入口探测。
try {
let isPythonWeb = false;
// 不限量扫描关键 Web 入口信号(.py 全量遍历文件头,maxFiles 提到 400 覆盖大项目)
const pyAll = scanSourceFiles(dir, ['.py'], 400, 7);
const pyHead = pyAll.map(f => f.head).join('\n');
isPythonWeb = /(FastAPI\(|Flask\(__name__\)|app\s*=\s*(FastAPI|Flask)\(|@app\.(get|post|put|delete|route)|uvicorn\.run|streamlit\.run|Django|django)/i.test(pyHead);
if (isPythonWeb) {
// 在 static/templates/public 目录下找 .html 文件(这些是 Web 应用的页面承载)
const htmlFiles: string[] = [];
const walkStatic = (d: string, depth: number) => {
if (depth > 6) return;
let entries: any[] = [];
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
if (e.name.startsWith('.') || WEBMODE_SKIP_DIRS.includes(e.name)) continue;
const p = path.join(d, e.name);
if (e.isDirectory()) { walkStatic(p, depth + 1); continue; }
if (/\.html?$/i.test(e.name)) htmlFiles.push(path.relative(dir, p).replace(/\\/g, '/'));
}
};
walkStatic(dir, 0);
const realHtml = htmlFiles.filter(rel => !/coverage|dist|build|node_modules/.test(rel));
if (realHtml.length > 0) {
reasons.push(`Web 框架 + 页面文件(${realHtml.slice(0, 3).join(', ')}`);
}
}
} catch { }
// 1. 源码级前端框架(排除产物目录)
const frontendSrcExts = ['.tsx', '.jsx', '.vue', '.svelte'];
const srcFrontend = scanSourceFiles(dir, frontendSrcExts, 80, 6);
if (srcFrontend.length > 0) {
reasons.push(`前端组件源码(${srcFrontend.length} 个文件,如 ${srcFrontend.slice(0, 3).map(f => f.rel).join(', ')}`);
}
// 2. 前端构建配置
const webBuildConfigs = ['vite.config.ts', 'vite.config.js', 'vue.config.js', 'webpack.config.js', 'next.config.js', 'angular.json', 'react-scripts'];
for (const f of webBuildConfigs) {
if (fs.existsSync(path.join(dir, f))) { reasons.push(`${f} 前端构建配置`); break; }
}
// 3. 非产物的 index.html(排除 dist/build/coverage 下的)
const indexHtmlReasons: string[] = [];
const walkIndex = (d: string, depth: number) => {
if (depth > 6) return;
let entries: any[] = [];
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
if (e.name.startsWith('.') || WEBMODE_SKIP_DIRS.includes(e.name)) continue;
const p = path.join(d, e.name);
if (e.isDirectory()) { walkIndex(p, depth + 1); continue; }
if (e.name.toLowerCase() === 'index.html') {
const rel = path.relative(dir, p).replace(/\\/g, '/');
if (!/coverage|dist|build|node_modules/.test(rel)) indexHtmlReasons.push(rel);
}
}
};
walkIndex(dir, 0);
if (indexHtmlReasons.length > 0) reasons.push(`非产物 index.html${indexHtmlReasons.slice(0, 3).join(', ')}`);
// 4. package.json 前端依赖(react/vue/next
try {
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
if (deps['react'] || deps['vue'] || deps['next'] || deps['@vitejs/plugin-react'] || deps['svelte']) {
reasons.push('前端框架依赖(react/vue/next/svelte');
}
}
} catch { }
const hasFrontend = reasons.length > 0;
// CLI / VS Code 插件豁免:仅在无任何真实前端信号时生效(纯 CLI/插件不要求 GUI);
// 有真实前端信号的项目仍按真实前端处理(避免 bin 字段误豁免有 UI 的项目)
const cliExempt = !hasFrontend && cliOrExtension;
return {
hasFrontend: hasFrontend || cliExempt,
reason: hasFrontend
? `真实前端(${reasons.join('')}`
: (cliExempt
? 'CLI 工具 / VS Code 插件(IDE 集成或命令行交互即其界面,豁免前端要求)'
: '未发现真实前端界面(仅后端服务/静态产物;前端存在性未能由源码确认)'),
cliOrExtension: cliExempt,
};
}
export interface WebModeInfo {
hasWeb: boolean;
mode: 'web' | 'cli';
@@ -1221,6 +1556,26 @@ async function executeReview(entryId: string) {
// 评审真实性 P1:Map-Reduce 全量功能发现(供实现类维度对照验收基准)
const featureInventoryText = await discoverFeatureInventory(entryId, files);
// 评审真实性(2026-08-27):文档声称-代码实现一致性校验
// 防"拿到报告后针对性补齐文档声称欺骗评审":校验文档声称的功能 ↔ 代码真实实现的一致性
const claimConsistencyText = await buildClaimConsistency(entryId, files);
// 代码质量确定性评估(2026-08-27):空壳率/重复率/测试有效度,防"光有数量没有质量"
const qualityResult = computeCodeQuality(files);
if (qualityResult.applicable) {
pipeLog(entryId, 'QUALITY', `stub=${Math.round(qualityResult.stubRatio * 100)}% dup=${Math.round(qualityResult.dupRatio * 100)}% testValidity=${Math.round(qualityResult.testValidityRatio * 100)}% score=${qualityResult.qualityScore}`);
}
// 评审真实性 P2:测量协议审计 + 验收命令执行(供效果/实现维度)
let effectAuditPrompt = '';
try {
const ma = auditMeasurement(dir);
effectAuditPrompt = ma.toPrompt;
pipeLog(entryId, 'MEASURE', `dir=${ma.dirExists} baseline=${ma.hasBaseline} timing=${ma.hasTimingLog}`);
} catch { }
const acceptResult = await runAccept(dir, 90000, process.env.NODE_ENV === 'test');
pipeLog(entryId, 'ACCEPT', `found=${acceptResult.found} ok=${acceptResult.ok} cmd=${acceptResult.command || '-'}`);
// 方案②:项目理解文档(AI 解读代码生成,落库供 B 阶段与黑盒冒烟复用)
const tUnderstand = Date.now();
const understanding = await buildProjectUnderstanding(entryId, dir, files, codeStats);
@@ -1342,10 +1697,20 @@ async function executeReview(entryId: string) {
: `系统确定性判定:存在演示视频文件 ${videoDet.files.join('、')}(评审系统不解析视频内容,该子项按存在性计分,AI 不评审视频)`)
: '系统确定性判定:未发现演示视频文件(mp4/mov/webm)及链接,该子项计 0 分';
const ideNote = entry.category_tag === '赛道二' ? extractIdeContributions(dir) : '';
// 真实前端判定(2026-08-31):单阶段流程注入前端形态,让 AI 在实现完整度维度据实评分
const frontendDetSingle = detectFrontend(dir);
const feReasonSingle = frontendDetSingle.hasFrontend
? frontendDetSingle.reason
: (frontendDetSingle.cliOrExtension
? frontendDetSingle.reason + 'CLI/插件豁免,不要求 GUI'
: frontendDetSingle.reason + ' → 无真实前端,实现完整度维度应封顶');
const frontendNoteSingle = '前端形态判定: ' + feReasonSingle
+ (((entry.build_status || '').trim() === 'done_no_ui') ? '\n ⚠️评委人工确认构建成功但无前端界面(done_no_ui' : '');
const projectContext = [
`项目标题: ${entry.title}`,
`仓库地址: ${entry.repo_url}`,
serviceUrlNoteFull,
frontendNoteSingle,
`代码统计: ${codeStats.fileCount}文件,${codeStats.totalLines}行代码`,
Object.entries(codeStats.languageStats).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([lang, lines]) => ` ${lang}: ${lines}`).join('\n'),
'', '=== 代码健康度 ===', codeHealth,
@@ -1396,7 +1761,7 @@ ${overviewFileBlock}
const dimensions: any[] = [];
const toRun = [...standardDims];
const l2ExtraContexts = buildL2ExtraContexts(entry);
const auditExtra = buildAuditExtraContexts(entryId, standardDims, files, featureInventoryText);
const auditExtra = buildAuditExtraContexts(entryId, standardDims, files, featureInventoryText + renderAcceptToPrompt(acceptResult), effectAuditPrompt, claimConsistencyText, qualityResult?.applicable ? qualityResult.toPrompt : '');
const mergedExtra = Object.keys(auditExtra).length > 0 ? { ...l2ExtraContexts, ...auditExtra } : l2ExtraContexts;
const runNext = async () => {
while (toRun.length > 0) {
@@ -1515,10 +1880,22 @@ ${JSON.stringify(dimensions.map(d => ({ name: d.name, score: d.score, maxScore:
const hasRootReadme = files.some(f => path.dirname(f.path) === '.' && /readme\.md$/i.test(path.basename(f.path)));
const testStepFailed = buildResult.steps.some(s => s.status === 'fail' && s.command.includes('pytest'));
const buildFailed = !buildResult.canBuild && !serviceUrl && !buildResult.untested;
// 真实前端判定(2026-08-31):一体化流程(赛道二/L2单阶段)也生效——
// web 形态但无真实前端源码,或 web 形态 + 有前端但不可访问(service_url 缺失/打不开)
// → 实现完整度/稳定性封顶 50%(CLI/插件豁免)
const manualBuildStatus = (entry.build_status || '').trim();
const frontendDet = detectFrontend(dir);
const feWeb = detectWebMode(dir).hasWeb;
const frontendUnverifiableSingle = feWeb && frontendDet.hasFrontend && !frontendDet.cliOrExtension
&& (!serviceUrl || !browseResult?.pageLoaded);
const frontendAbsent = manualBuildStatus === 'done_no_ui'
|| ((manualBuildStatus !== 'failed') && feWeb && !frontendDet.hasFrontend && !frontendDet.cliOrExtension)
|| frontendUnverifiableSingle;
const claimRatio = getClaimConsistencyRatio(entryId);
const { dimensions: cappedDims, log: hardRulesLog } = applyTrackHardRules(
getProjectTrack(entry.project_id),
dimensions.map(d => ({ name: d.name, score: d.score, maxScore: d.maxScore })),
{ buildFailed, testStepFailed, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme }
{ buildFailed, testStepFailed, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme, claimConsistencyRatio: claimRatio, stubRatio: qualityResult?.applicable ? qualityResult.stubRatio : undefined, testValidityRatio: qualityResult?.applicable ? qualityResult.testValidityRatio : undefined, frontendAbsent, frontendCliOrExtension: frontendDet.cliOrExtension }
);
for (const cd of cappedDims) {
const target = dimensions.find(d => d.name === cd.name);
@@ -1681,7 +2058,7 @@ ${overviewFileBlockA}
const dimensionsA: any[] = [];
const toRunA = [...aDims];
const l2ExtraContextsA = buildL2ExtraContexts(entry);
const auditExtraA = buildAuditExtraContexts(entryId, aDims, files, featureInventoryText);
const auditExtraA = buildAuditExtraContexts(entryId, aDims, files, featureInventoryText + renderAcceptToPrompt(acceptResult), effectAuditPrompt, claimConsistencyText, qualityResult?.applicable ? qualityResult.toPrompt : '');
const mergedExtraA = Object.keys(auditExtraA).length > 0 ? { ...l2ExtraContextsA, ...auditExtraA } : l2ExtraContextsA;
const runNextA = async () => {
while (toRunA.length > 0) {
@@ -1741,7 +2118,7 @@ ${JSON.stringify(dimensionsA.map(d => ({ name: d.name, score: d.score, maxScore:
const { dimensions: cappedDimsA, log: hardRulesLogA } = applyTrackHardRules(
getProjectTrack(entry.project_id),
dimensionsA.map(d => ({ name: d.name, score: d.score, maxScore: d.maxScore })),
{ buildFailed: false, testStepFailed: false, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme }
{ buildFailed: false, testStepFailed: false, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme, claimConsistencyRatio: getClaimConsistencyRatio(entryId), stubRatio: qualityResult?.applicable ? qualityResult.stubRatio : undefined, testValidityRatio: qualityResult?.applicable ? qualityResult.testValidityRatio : undefined }
);
for (const cd of cappedDimsA) {
const target = dimensionsA.find(d => d.name === cd.name);
@@ -1782,6 +2159,17 @@ ${JSON.stringify(dimensionsA.map(d => ({ name: d.name, score: d.score, maxScore:
db.prepare("UPDATE entries SET status = 'a_done', ai_report = ?, score_a = ?, stage_b_status = 'pending', progress_log = json(?), updated_at = datetime('now') WHERE id = ?").run(
JSON.stringify(aiReportA), scoreA, JSON.stringify(aLogs), entryId);
// A 阶段报告备份(2026-08-31 修复):B 阶段会把 entry.ai_report 覆盖为 B 维度,
// 若中途 awaiting_browse 恢复导致 B 重复执行,A 维度明细会丢失。这里把 A 报告独立备份到
// feature_inventory 字段(JSON 容器),B 阶段从备份恢复 A 维度,避免维度丢失/重复累积。
try {
const feat = (() => { try { return JSON.parse((db.prepare('SELECT feature_inventory FROM entries WHERE id = ?').get(entryId) as any)?.feature_inventory || '{}'); } catch { return {}; } })();
if (typeof feat === 'object' && !Array.isArray(feat)) {
feat.__stageA_report = aiReportA;
db.prepare('UPDATE entries SET feature_inventory = ? WHERE id = ?').run(JSON.stringify(feat), entryId);
}
} catch { }
pipeLog(entryId, 'DONE_A', `scoreA=${scoreA}/${maxScoreA} (${pctA}%) waiting system verify`, Date.now() - t0);
// 保留 clone 目录供 B 阶段复用,A/B 评同一份代码
}
@@ -1791,9 +2179,19 @@ const EMPTY_BUILD_RESULT: BuildResult = { canBuild: false, untested: false, step
// B 阶段:构建后评审(verify 触发)。复用 clone 目录 + tryBuild/tryTest/tryBrowse
// 只评 B 维度子 Agent(校准注入 A 维度分)+ B 硬规则 + scoreB + 合并 A+B + finalScore
async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' = 'done') {
async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' | 'done_no_ui' = 'done') {
const entry = db.prepare('SELECT * FROM entries WHERE id = ?').get(entryId) as any;
if (!entry || entry.status !== 'verifying') return;
// 2026-08-31:允许从 awaiting_browse(评审环境自动启动失败,评委手动启动后填 service_url 恢复)进入 B 阶段
if (!entry || (entry.status !== 'verifying' && entry.status !== 'awaiting_browse')) return;
// 评委人工确认标志:从 awaiting_browse 恢复时,后续浏览器验证信任评委填的地址(跳过 SSRF 内网拦截)
let trustedBrowse = false;
if (entry.status === 'awaiting_browse') {
// 从等待恢复:确认已填 service_url 才继续;未填则保持等待
if (!(entry.service_url || '').trim()) return;
trustedBrowse = true;
db.prepare("UPDATE entries SET status = 'verifying' WHERE id = ?").run(entryId);
entry.status = 'verifying';
}
const t0 = Date.now();
pipeLog(entryId, 'START_B', `repo=${entry.repo_url} track=${entry.category_tag || '?'} build_status=${buildStatus}`);
@@ -1806,9 +2204,35 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
const stat = fs.statSync(dir);
if (!stat.isDirectory()) throw new Error('目录不存在');
files = discoverFiles(dir) as any[];
if (files.length === 0) throw new Error('目录为空');
// 2026-08-27:不再因 files 为空抛错——二进制成果物(.pptx 等)不被 discoverFiles 收录,
// 目录有 git 内容但无源码文本文件时继续 B 阶段评审(AI 基于项目元信息评,实现类维度自然低分)
codeStats = countCodeStats(dir);
} catch (e: any) {
// 空仓库(git 初始化但无工作树文件):终局 0 分,不报错回滚(2026-08-27
// 判据:目录存在且除 .git 外无任何条目(选手未提交任何成果物)。
// 注意不能用 discoverFiles 为空判定——二进制成果物(.pptx 等)不被 discoverFiles 收录,
// 但目录仍有内容(如 经营管理 .git+ppt → 不是空仓库,应继续 B 阶段评审)
try {
const stat = fs.statSync(dir);
if (stat.isDirectory()) {
const entriesInDir = fs.readdirSync(dir);
const hasOnlyGit = entriesInDir.length === 1 && entriesInDir[0] === '.git';
const isBareEmpty = entriesInDir.length === 0;
if (hasOnlyGit || isBareEmpty) {
pipeLog(entryId, 'EMPTY_REPO', '空仓库(仅 .git 无成果物),终局 0 分');
const emptyReport = { dimensions: [], totalScore: 0, maxTotal: 100, pct: 0, raw: '', calibrationExplanation: '', overall: null as any, emptyRepo: true };
const emptyLogs = [{ time: new Date().toISOString(), status: 'review_done', msg: '仓库为空(未提交代码成果物),按 0 分处理' }];
db.transaction(() => {
db.prepare(`UPDATE entries SET status = 'review_done', ai_report = ?, raw_score = 0, final_score = 0, final_level = '不合格', score_a = COALESCE(score_a, 0), score_b = 0, stage_b_status = 'done', progress_log = json(?), updated_at = datetime('now') WHERE id = ?`).run(
JSON.stringify(emptyReport), JSON.stringify(emptyLogs), entryId);
db.prepare('INSERT INTO review_snapshots (id, entry_id, attempt, ai_report, standard_snapshot, score) VALUES (?, ?, ?, ?, ?, ?)').run(
crypto.randomUUID(), entryId, entry.attempt || 1, JSON.stringify(emptyReport), entry.standard_snapshot || '', 0);
})();
pipeLog(entryId, 'DONE_B', 'score=0/100 empty-repo final=0', 0);
return;
}
}
} catch { }
pipeLog(entryId, 'FAIL_B', `context-expired: ${e.message}`);
throw new Error('评审上下文已过期,请重新评审');
}
@@ -1820,13 +2244,34 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
// L2考核:跨仓库查重初筛(确定性 flags,供人工复核,不自动定罪)
await runPlagiarismScreening(entry, dir);
// 人工构建确认2026-08-16):系统不再自动 tryBuild,评委确认构建结果
// 构造合法 buildResult 供子 Agent/硬规则使用:failed → canBuild=false(触发 B 维度封顶);done → 不证伪
const buildFailed = buildStatus === 'failed';
const buildResult: BuildResult = buildFailed
? { canBuild: false, untested: false, steps: [], summary: '评委人工确认构建失败,未执行自动构建(系统不再自动 tryBuild' }
: { canBuild: true, untested: true, steps: [], summary: '评委人工确认构建成功(系统不再自动 tryBuild' };
pipeLog(entryId, 'BUILD_B', `manual-confirm buildFailed=${buildFailed}`);
// 构建验证2026-08-27 恢复赛制原设计:人工构建确认为主):
// A 阶段结束后由评委/选手实际构建,verify 传入 build_statusdone/failed)作为 B 阶段判定依据。
// - done → 人工确认构建成功 → 不封顶(即使评审环境自动构建失败,尊重人工确认——环境差异不误杀)
// - failed → 人工确认构建失败 → 触发 B 维度封顶
// 系统自动 tryBuild 仅作辅助证据注入 prompt(供 AI 参考报错/环境问题),不覆盖人工确认判定。
const tBuildB = Date.now();
let buildResult: BuildResult;
if (buildStatus === 'failed') {
buildResult = { canBuild: false, untested: false, steps: [], summary: '评委人工确认构建失败,触发 B 维度封顶' };
} else if (buildStatus === 'done_no_ui') {
// 评委确认构建成功但无前端界面(2026-08-31):构建判定成功(不触发构建失败封顶),
// 但前端存在性由人工确认为缺失 → 走前端缺失封顶(frontendAbsent
const autoBuild = await tryBuild(dir);
const autoNote = autoBuild.canBuild
? `自动构建验证通过:${autoBuild.summary}`
: `(评审环境自动构建未通过:${autoBuild.summary};以评委人工确认为准)`;
buildResult = { canBuild: true, untested: autoBuild.untested, steps: autoBuild.steps, summary: '评委人工确认构建成功但无前端界面(done_no_ui)。' + autoNote };
pipeLog(entryId, 'BUILD_B', `manual=done_no_ui auto-canBuild=${autoBuild.canBuild} untested=${autoBuild.untested}`, Date.now() - tBuildB);
} else {
// 人工确认成功(done)或未传(默认 done 语义):记录自动构建结果作参考,但判定为可构建
const autoBuild = await tryBuild(dir);
const autoNote = autoBuild.canBuild
? `自动构建验证通过:${autoBuild.summary}`
: `(评审环境自动构建未通过:${autoBuild.summary};以评委人工确认为准)`;
buildResult = { canBuild: true, untested: autoBuild.untested, steps: autoBuild.steps, summary: '评委人工确认构建成功。' + autoNote };
pipeLog(entryId, 'BUILD_B', `manual=done auto-canBuild=${autoBuild.canBuild} untested=${autoBuild.untested}`, Date.now() - tBuildB);
}
const buildFailed = !buildResult.canBuild;
const tTest = Date.now();
const testEvidence: any = await tryTest(dir);
@@ -1836,6 +2281,16 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
const serviceUrl = (entry.service_url || '').trim();
const webMode = resolveWebMode(entryId, dir);
// 真实前端判定(2026-08-31):区分「有真实前端」vs「仅后端服务/静态产物」。
// done_no_ui(评委人工确认无前端)或系统判定 web 形态但无前端源码 → frontendAbsent
const frontend = detectFrontend(dir);
const frontendAbsent = buildStatus === 'done_no_ui'
|| (webMode.hasWeb && !frontend.hasFrontend && !frontend.cliOrExtension);
const frontendNote = '前端形态判定: ' + (frontend.hasFrontend
? frontend.reason
: (frontend.cliOrExtension ? frontend.reason + '(豁免前端要求)' : frontend.reason + ' → 触发实现完整度封顶'))
+ (buildStatus === 'done_no_ui' ? '\n ⚠️评委人工确认构建成功但无前端界面(done_no_ui' : '');
pipeLog(entryId, 'FRONTEND', `hasFrontend=${frontend.hasFrontend} cliOrExt=${frontend.cliOrExtension} absent=${frontendAbsent} buildStatus=${buildStatus || 'done'}`);
const videoDet = detectDemoVideo(dir);
const tBrowse = Date.now();
let startResult: any, browseResult: any;
@@ -1845,7 +2300,7 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
browseResult = { tested: false, pageLoaded: false, jsErrors: [], networkErrors: [], summary: '构建失败,跳过浏览器测试' };
} else if (webMode.hasWeb) {
if (serviceUrl) {
browseResult = await tryBrowse(serviceUrl, true, dir, entryId);
browseResult = await tryBrowse(serviceUrl, true, dir, entryId, trustedBrowse);
if (browseResult.pageLoaded) {
startResult = { started: true, url: serviceUrl, port: 0, logs: '已通过参赛者提供的服务地址访问' };
} else {
@@ -1856,9 +2311,23 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
smokeEvidence = await trySmoke(serviceUrl, entry.project_understanding || '');
}
} else {
// hasWeb 但未给 service_url → verify 已 400 拦截;兜底视为无法访问
startResult = { started: false, url: '', port: 0, logs: '判定为 Web 形态但未提供服务地址,跳过浏览器测试' };
browseResult = { tested: false, pageLoaded: false, jsErrors: [], networkErrors: [], summary: '未提供服务地址,跳过浏览器测试' };
// hasWeb 但未给 service_url2026-08-31 增强):先尝试本地 tryStart 启动,
// 启动成功则 browse 验证;失败则【暂停等待评委手动启动】(2026-08-31 方案):
// 评审环境可能缺依赖/API key 导致启动失败(不代表项目不能跑),
// 反馈启动命令与失败原因,评委手动启动后填 service_url 重新验证。
startResult = await tryStart(dir);
if (startResult.started) {
browseResult = await tryBrowse(startResult.url, true, dir, entryId);
} else {
// 暂停等待人工启动:置 awaiting_browse,反馈启动命令与失败原因。
// 评委手动启动后填 service_url → 走 executeReviewBContinue 继续浏览器验证 + 评分。
const startCmd = resolveStartCommand(dir);
const awaitingLogs = (() => { try { return JSON.parse(entry.progress_log || '[]'); } catch { return []; } })();
awaitingLogs.push({ time: new Date().toISOString(), status: 'awaiting_browse', msg: '评审环境无法自动启动服务(可能缺依赖/API key),已暂停等待评委手动启动。请手动执行启动命令并填写服务地址。' });
db.prepare(`UPDATE entries SET status = 'awaiting_browse', stage_b_status = 'awaiting', progress_log = json(?) WHERE id = ?`).run(JSON.stringify(awaitingLogs), entryId);
pipeLog(entryId, 'AWAIT_BROWSE', `startCmd=${startCmd?.command || '(未识别)'} logs=${(startResult.logs || '').slice(0, 200)}`);
return;
}
}
} else {
startResult = await tryStart(dir);
@@ -1866,6 +2335,45 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
}
pipeLog(entryId, 'BROWSE_B', `serviceUrl=${serviceUrl || '(none)'} hasWeb=${webMode.hasWeb} pageLoaded=${browseResult.pageLoaded} started=${startResult.started} smoke=${smokeEvidence?.tested ? (smokeEvidence.goals?.length || 0) : 'skipped'}`, Date.now() - tBrowse);
// 前端缺失最终判定(2026-08-31):「有了不能用,跟没有一样」——
// 在 browse 结果出来后,把「web 形态 + 有前端 + 前端不可访问(service_url 缺失/无法打开)」也纳入 frontendAbsent
// 1. 评委人工确认无前端(done_no_ui)
// 2. web 形态但无真实前端源码(系统判定)
// 3. web 形态 + 有前端源码,但 service_url 缺失 或 浏览器测试 pageLoaded=false(前端存在但未验证可用)
// CLI/插件豁免(纯命令行/IDE 插件,其界面即交互方式)不触发任何前端封顶。
const frontendUnverifiable = webMode.hasWeb && frontend.hasFrontend && !frontend.cliOrExtension
&& (!serviceUrl || !browseResult.pageLoaded);
const frontendAbsentFinal = buildStatus === 'done_no_ui'
|| (webMode.hasWeb && !frontend.hasFrontend && !frontend.cliOrExtension)
|| frontendUnverifiable;
if (frontendUnverifiable && !frontendAbsent) {
pipeLog(entryId, 'FRONTEND', `frontend exists but unverifiable: serviceUrl=${serviceUrl || '(none)'} pageLoaded=${browseResult.pageLoaded} → 触发实现完整度封顶`);
}
// 文档-实现对照增强(2026-08-31):B 阶段运行时证据齐备后,重算 claim-consistency。
// A 阶段只对照"代码里有没有"(静态);这里注入构建/测试/前端访问结果,
// 让 AI 识别"代码有但跑不起来"的声称(动态失效)——三级真实性校验。
// 重算结果覆盖落库 claim_consistency_json,供 B 维度评分与文档类维度使用。
if (config.deepseekApiKey) {
try {
const files2 = discoverFiles(dir) as any[];
const runtimeEvidence = {
buildOk: !buildFailed,
buildSummary: buildResult.summary,
testsPassed: !!testEvidence?.tested && !!testEvidence?.passed,
testPassedCount: testEvidence?.testsPassed,
testRunCount: testEvidence?.testsRun,
testSummary: testEvidence?.summary,
webAccessible: browseResult?.pageLoaded === true,
webSummary: browseResult?.summary,
};
const claimText = await buildClaimConsistency(entryId, files2, runtimeEvidence);
if (claimText) pipeLog(entryId, 'CLAIM_B', 'runtime-enhanced claim-consistency recomputed');
} catch (e: any) {
pipeLog(entryId, 'CLAIM_B', `recompute failed: ${e.message}`);
}
}
// 解析标准维度快照,分离 B 阶段维度(stage === 'B'
let standardDims: any[] = [];
try { standardDims = JSON.parse(entry.standard_snapshot || '[]'); } catch { standardDims = []; }
@@ -1880,9 +2388,21 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
let aOverview = '';
let aDimensions: any[] = [];
try {
const aReport = JSON.parse(entry.ai_report || '{}');
aOverview = aReport.overview || '';
if (Array.isArray(aReport.dimensions)) aDimensions = aReport.dimensions;
// 2026-08-31 修复:优先从 feature_inventory 的 __stageA_report 备份读取 A 阶段维度
// entry.ai_report 在 B 执行后会被覆盖为 B 维度,从 awaiting_browse 恢复时读不到 A 维度)。
let aReport: any = null;
try {
const feat = JSON.parse((db.prepare('SELECT feature_inventory FROM entries WHERE id = ?').get(entryId) as any)?.feature_inventory || '{}');
if (feat && typeof feat === 'object' && !Array.isArray(feat) && feat.__stageA_report) aReport = feat.__stageA_report;
} catch { }
if (!aReport) {
const parsed = JSON.parse(entry.ai_report || '{}');
if (parsed.stage !== 'B') aReport = parsed; // 未被 B 覆盖时直接用当前报告
}
if (aReport) {
aOverview = aReport.overview || '';
if (Array.isArray(aReport.dimensions)) aDimensions = aReport.dimensions;
}
} catch { /* 忽略 */ }
const scoreA = Number(entry.score_a || 0);
@@ -1909,6 +2429,7 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
`仓库地址: ${entry.repo_url}`,
serviceUrlNote,
webModeNote,
frontendNote,
`代码统计: ${codeStats.fileCount}文件,${codeStats.totalLines}行代码`,
Object.entries(codeStats.languageStats).sort((a: any, b: any) => b[1] - a[1]).slice(0, 5).map(([lang, lines]) => ` ${lang}: ${lines}`).join('\n'),
'', '=== 代码健康度 ===', codeHealth,
@@ -1933,7 +2454,10 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' =
const dimensionsB: any[] = [];
const toRunB = [...bDims];
const l2ExtraContextsB = buildL2ExtraContexts(entry);
const auditExtraB = buildAuditExtraContexts(entryId, bDims, files, '');
// B 阶段复用 A 阶段已落库的功能发现统计(不重算 LLM,确定性证据注入实现完整度维度)
const featureInvB = renderStoredFeatureEvidence(entry);
const claimB = renderStoredClaimConsistency(db.prepare('SELECT * FROM entries WHERE id = ?').get(entryId) as any);
const auditExtraB = buildAuditExtraContexts(entryId, bDims, files, featureInvB, '', claimB);
const mergedExtraB = Object.keys(auditExtraB).length > 0 ? { ...l2ExtraContextsB, ...auditExtraB } : l2ExtraContextsB;
const runNextB = async () => {
while (toRunB.length > 0) {
@@ -2023,7 +2547,7 @@ ${JSON.stringify(dimensionsB.map(d => ({ name: d.name, score: d.score, maxScore:
const { dimensions: cappedDimsB, log: hardRulesLogB } = applyTrackHardRules(
getProjectTrack(entry.project_id),
dimensionsB.map(d => ({ name: d.name, score: d.score, maxScore: d.maxScore })),
{ buildFailed, testStepFailed, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme }
{ buildFailed, testStepFailed, duplicateRatio: codeStats.duplicateRatio, hasAnyReadme, hasRootReadme, claimConsistencyRatio: getClaimConsistencyRatio(entryId), frontendAbsent: frontendAbsentFinal, frontendCliOrExtension: frontend.cliOrExtension }
);
for (const cd of cappedDimsB) {
const target = dimensionsB.find(d => d.name === cd.name);
@@ -2658,6 +3182,8 @@ async function runSubAgent(dim: any, projectContext: string, files: { path: stri
? '\n\n【运行边界】评审环境无 IDE 宿主(无法实跑插件),本维度请按静态证据评分:错误处理/降级/重试机制、依赖与一键安装可行性(见"构建确认"与"IDE 贡献点")、代码可读性。不得因"评审环境无法运行插件"而额外扣分(那是环境限制,非作品缺陷)。'
: '';
const isImplDim = /功能完整|实现完整|规模|功能点/.test(dim.name);
const returnFields = `"name": "${dim.name}", "score": 分数, "comment": "纯文字总结(禁止代码,200字内)", "suggestion": "改进建议"${isImplDim ? `, "checks": [{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub","evidence":"代码原文摘录","reason":"判定理由"}](至少1条,按上方定向追踪任务的核查结果如实填写)` : ''}`;
const prompt = `你是一个AI大赛评审专家,请只评审以下一个维度:
${projectContext}
@@ -2678,7 +3204,7 @@ ${(dim.name.includes('实现完整') || dim.name.includes('效果')) && smokeEvi
- ★维度独立原则:本维度必须独立评分。**其他维度的判定(如 Agent核心门槛是否通过、项目是否为 Agent 应用)不构成对本维度的扣分依据**。例如:效果与数据评估的是测试覆盖/覆盖率/可复现性,即使项目不是 Agent 项目,只要测试真实存在且通过,就应据实给分,不得以"非 Agent"而否决${dim.name.includes('效果') && testEvidence?.passed ? '。本维度已提供真实运行的测试结果(通过+覆盖率),评分必须以上述真实数字为主要依据,不得忽略' : ''}
返回严格JSON:
{"name": "${dim.name}", "score": 分数, "comment": "纯文字总结(禁止代码,200字内)", "suggestion": "改进建议"}`;
{${returnFields}}`;
let raw = await callDeepSeek(prompt);
if (!raw) {
+16 -3
View File
@@ -198,9 +198,18 @@ export function computeCalibration(
/**
* Agent §3.3.5JSON codeblock JSON
*/
export interface DimCheck { feature: string; entry: string; depth: 'real' | 'partial' | 'stub'; evidence: string; reason: string; }
export function parseDimResponse(raw: string, dim: { name: string; maxScore: number; group?: string }): {
name: string; score: number; maxScore: number; comment: string; suggestion: string; group: string;
name: string; score: number; maxScore: number; comment: string; suggestion: string; group: string; checks?: DimCheck[];
} {
const normalizeChecks = (v: unknown): DimCheck[] | undefined => {
if (!Array.isArray(v) || v.length === 0) return undefined;
const valid = v.filter(x => x && typeof x === 'object' && typeof (x as any).feature === 'string' && ['real', 'partial', 'stub'].includes((x as any).depth));
return valid.length ? valid.map(x => ({
feature: (x as any).feature, entry: String((x as any).entry || ''), depth: (x as any).depth,
evidence: String((x as any).evidence || ''), reason: String((x as any).reason || ''),
})) : undefined;
};
try {
const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
const jsonStr = jsonMatch ? jsonMatch[1].trim() : raw.trim();
@@ -212,6 +221,7 @@ export function parseDimResponse(raw: string, dim: { name: string; maxScore: num
comment: (parsed.comment || '').replace(/```[\s\S]*?```/g, '').trim(),
suggestion: parsed.suggestion || '',
group: dim.group || 'common',
checks: normalizeChecks(parsed.checks),
};
} catch {
const scoreMatch = raw.match(/"score":\s*(\d+)/);
@@ -253,9 +263,12 @@ export function aggregateEntryScores(
): { 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));
// 只看最近 N 条的标准一致性(历史旧标准快照不参与当前聚合窗口的判定),
// 否则"三轮新标准 + 历史旧标准并存"会导致永远无法聚合(2026-08-27 修复)
const recent = rows.slice(-recentN);
const stds = new Set(recent.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);
const scores = recent.map(r => r.score as number);
return aggregateScores(scores);
}
+15
View File
@@ -123,6 +123,21 @@ export async function tryTest(dir: string): Promise<TestEvidence> {
return { tested: false, command: '', passed: false, testsRun: 0, testsPassed: 0, testsFailed: 0, coverage: null, summary: '未检测到测试框架配置' };
}
// 依赖预装(2026-08-31):Python 项目测试常因依赖未安装(ImportError)而失败,
// 评审环境是全新 clone 目录,没有预先 pip install。跑测试前先装依赖(静默,失败不阻断测试)。
// 仅对 Python 构建系统执行,避免影响 npm/maven 等(其依赖管理更重)。
try {
const hasPy = fs.existsSync(path.join(dir, 'pyproject.toml')) || fs.existsSync(path.join(dir, 'requirements.txt')) || fs.existsSync(path.join(dir, 'setup.py'));
if (hasPy && candidates[0].file === 'pyproject.toml') {
const installCmd = fs.existsSync(path.join(dir, 'requirements.txt'))
? 'python -m pip install -q -r requirements.txt'
: 'python -m pip install -q -e .';
try {
execSync(installCmd, { cwd: dir, timeout: 180000, stdio: 'pipe', encoding: 'utf-8' });
} catch { /* 依赖安装失败不阻断,测试结果据实反映 */ }
}
} catch { }
// 取第一个有 test 命令的构建系统
const { cmd, relDir } = candidates[0];
const cwd = relDir ? path.join(dir, relDir) : dir;
-50
View File
@@ -1,50 +0,0 @@
const BASE = 'http://localhost:3002';
const PID = 'b1b5884e-ba85-4d8f-9a92-e524603587a0';
async function main() {
const login = await fetch(`${BASE}/api/auth/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: '620f4c96' })
});
const { token } = await login.json();
const auth = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` };
const r = await fetch(`${BASE}/api/projects/${PID}/entries`, {
method: 'POST', headers: auth,
body: JSON.stringify({ title: 'cobol-java-subagent', repo_url: 'file://D:/Projects/cobol-java/jcl-cobol-git', participant: 'hangshuo' })
});
const entry = await r.json();
if (!r.ok) { console.log('FAIL:', JSON.stringify(entry)); process.exit(1); }
const eid = entry.id;
console.log('CREATE:', eid.slice(0, 8));
await fetch(`${BASE}/api/projects/${PID}/entries/${eid}/start`, { method: 'POST', headers: auth });
console.log('START OK');
const start = Date.now();
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const r3 = await fetch(`${BASE}/api/projects/${PID}/entries/${eid}`, { headers: auth });
const e2 = await r3.json();
console.log(`POLL_${i}[${Math.round((Date.now()-start)/1000)}s]: ${e2.status} RAW=${e2.raw_score}`);
if (e2.status === 'review_done') {
const report = typeof e2.ai_report === 'string' ? JSON.parse(e2.ai_report) : e2.ai_report;
console.log(`\n=== 11子Agent结果 (${Math.round((Date.now()-start)/1000)}s) ===`);
let st = 0, mt = 0;
for (const d of report.dimensions) {
st += d.score; mt += d.maxScore;
console.log(`[${d.score}/${d.maxScore}] ${d.name}`);
console.log(` ${(d.comment || '').slice(0, 120)}`);
}
console.log(`\nTotal: ${st}/${mt} = ${Math.round(st/mt*100)}%`);
break;
}
if (e2.status === 'failed' || e2.status?.includes('fail')) {
const logs = e2.progress_log ? JSON.parse(typeof e2.progress_log === 'string' ? e2.progress_log : '[]') : [];
console.log('FAIL:', JSON.stringify(logs.slice(-2)));
break;
}
}
}
main().catch(console.error);