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

- 前端存在性硬规则: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
+3
View File
@@ -19,7 +19,10 @@ __pycache__/
# heavy working data
server/data/
ai-review/server/data/
data/
server/clone/
server/.coverage
.coverage
.playwright-mcp/
screenshots/
+10 -1
View File
@@ -57,9 +57,11 @@ Phase 3: 确定性校准(computeCalibration) + 硬规则引擎
| `standards.ts` | `parseDimensions` 解析标准 MD 为维度列表 |
| `l2-topics.ts` | L2考核选题元数据(11命题题+自选题,`config/l2-topics.json`):难度赋分 cap、功能完整性拆分、验收要点注入 |
| `l2-participants.ts` | L2受验者注册表(`config/l2-participants.json`):员工编号↔Gitea账号↔仓库映射,自动生成拉取 URL;拉取认证走全局评审账号只读协作者权限 |
| `code-inventory.ts` | 全量符号索引(评审真实性改进 2026-08-26):对所有文本文件提取 path/lines/symbols,注入实现类维度让 AI 看见 100% 文件清单 |
| `code-inventory.ts` | 全量符号索引(评审真实性改进 2026-08-26):对所有文本文件提取 path/lines/symbols,注入实现类维度让 AI 看见 100% 文件清单(接口:`extractSignatures` + `renderInventoryText`,无 coreFiles 精读包——精读由 `filterFilesForDim` 维度过滤承担,2026-08-27 文档对齐) |
| `code-signals.ts` | 代码信号检测器:TODO 密度/空壳率/硬编码返回/死导入——确定性线索注入实现类维度 prompt,定位为线索而非判据 |
| `ai-log-audit.ts` | AI 日志确定性审计:占位率 / git 一致率 / 覆盖广度;占位>50%→维度≤30%、git一致<30%→≤20% 封顶 |
| `measurement-audit.ts` | 测量协议检查(P2,2026-08-27):确定性检查 `data/measurement/` 齐备性(baseline/README/timing-log),注入效果类维度 prompt |
| `accept-runner.ts` | 验收命令执行器(P22026-08-27):解析 README `## 验收` 命令块并真实执行,注入实现类维度;测试环境 dry-run 不实跑 |
| `entries.ts` | 条目 CRUD + 触发评审 + PDF 导出 |
| `projects.ts` | 项目 CRUD + 汇总排名 |
| `db.ts` | SQLite 初始化 + migrationsALTER TABLE try/catch |
@@ -173,6 +175,8 @@ cloneRepo → discoverFiles → countCodeStats → tryBuild → tryBrowse/trySta
- `UNIQUE(project_id, repo_url)` 约束
- `service_url TEXT DEFAULT ''`migration 添加)
- `feature_inventory TEXT DEFAULT ''`2026-08-27migration 添加):全量功能发现轮(Map-Reduce)结果 JSON 数组,供详情页展示与跨快照复用;重评时覆盖
- `ai_report.dimensions[n].checks`:实现类维度子 Agent 返回的定向追踪核对表(feature/entry/depth/evidence/reason),由 `parseDimResponse` 解析透传(2026-08-27 修正),前端维度表展示,缺失回退现状不崩
- `build_status TEXT DEFAULT ''`2026-08-18migration 添加):赛道二/L2考核单阶段人工构建确认,''=未确认(自动构建) / done / failed;赛道一 B 阶段不存此列,走 /verify 请求体
- `standard_snapshot` 存标准快照(评审时标准被修改也不影响已评审结果)
- `review_snapshots` 存每次评审历史
@@ -228,6 +232,11 @@ cd server && npm test -- src/__tests__/user-stories.test.ts
# E2E(需前后端都启动)
# web/e2e 目录有 Playwright 测试
## vitest 超时陷阱(2026-08-27 实测)
- **vitest 默认测试超时 5000ms**,而触发完整评审(`/start` → A 阶段)的用例因功能发现轮(Map-Reducemock 环境 12 块串行 ×400ms)拖到 ~5s,**会撞默认 5s 超时**。凡 `waitA_done`/等待 A 阶段完成的用例必须显式加超时(`}, 30000)` 或更大),否则偶发 `Test timed out in 5000ms`
- 现象:单跑通过、全量偶发失败,报错是 vitest 超时而非断言失败
## E2E 运行陷阱(2026-08-14 实测)
- **整套 full-e2e 需 15+ 分钟**,单个 describe 单独跑更快(评审标准 7、条目管理 10、批量/详情/汇总/异常/UI 27、完整流程 1 = 45 个测试)。整套跑超时是正常现象,不是 bug
+311
View File
@@ -0,0 +1,311 @@
# AI 评审机制说明
> 版本:2026-08-28 | 适用范围:技术大赛 赛道一 / 赛道二 | 读者:评委 / 管理员
**核心一句话**AI 评审 = **AI 维度打分 + 确定性证据校验**——AI 负责评"品质",代码负责查"真假",两者结合产出可审计、防作弊的评分。
**评审目标**:对参赛成果物给出**公平、真实、可区分**的评分——(1)**公平**:同一标准、同一证据链,不因 README 写得漂亮而虚高;(2)**真实**:分数建立在"系统能跑、功能真实现、测试真通过"的硬证据上,AI 幻觉与文档包装无法加分;(3)**可区分**:权重向"功能真实"倾斜,让真做了的团队与只交文档的团队拉开差距。
---
## 一图看懂评审管线
```
克隆 → 分析 → 概览 → 子Agent分维度评审(并发3)
│ ↓
│ 校准(代码定调幅) → 硬规则(证据封顶) → 出分
│ ↓
└→ 最近3次评审取中位数 → 聚合正式分 → 报告
```
**赛道一**(A/B 两阶段):A 静态评审 → 评委人工确认构建 → B 动态验证(实现/效果维度)
**赛道二**(单阶段):一次管线完成全部 8 维
---
## 目录
- [一图看懂评审管线](#一图看懂评审管线)
- [0. 核心特点](#0-核心特点)
- [1. 赛道一:Agent开发实战赛](#1-赛道一agent开发实战赛12维--200分)
- [2. 赛道二:IDE+开发范式创新赛](#2-赛道二ide开发范式创新赛8维--100分)
- [3. 确定性机制(保真实)](#3-确定性机制真实性验证)
- [4. 评分与可信度](#4-评分与可信度)
- [5. 评委操作流程](#5-评审流程操作评委视角)
- [6. 人工评审(发表会)](#6-人工评审发表会)
- [7. 已知边界](#6-已知边界与注意)
---
## 0. 核心特点
| 特点 | 说明 |
|:-----|:-----|
| 🎯 **真实性优先** | 功能真实类维度占赛道一 **42.5%**,文档类仅 **13.5%**——评分看"系统能不能跑、功能真不真",不看 README 写得多漂亮 |
| 🛡 **AI 不判真假** | 构建结果、测试通过率、功能占位率、日志占位率、文档一致性均由**代码确定性判定**,AI 只在证据上评品质,编造无效 |
| 🚫 **防作弊双保险** | 文档声称↔代码实现**一致性校验**(防补假文档)+ 空壳率/重复率/测试有效度**质量评估**(防空壳堆数量)。补真→加分,编造→降权 |
| 📊 **结果稳定** | 最近 3 次评审取**中位数**聚合(2 次取平均);标准快照隔离(评后改标准不影响已评结果) |
| 🔍 **可审计** | 每维度分带评语+证据引用;校准/硬规则改动写入 `calibrationExplanation`;历次快照可对比 |
| ⚡ **成本可控** | 并发3、功能发现分块扫描、确定性机制跨阶段复用,控制 LLM 调用量 |
---
## 1. 赛道一:Agent开发实战赛(12维 / 200分)
### 1.1 维度权重表
| # | 维度 | 分值 | 阶段 | 类型 |
|:-:|:-----|:----:|:----:|:----|
| 1 | 场景价值与技术合理性 | 15 | A | 设计/价值 |
| 2 | 开发范式应用 | 8 | A | 过程 |
| 3 | 架构设计 | 15 | A | 设计 |
| 4 | 工具使用与Skill集成深度 | 10 | A | 能力 |
| 5 | Agent核心能力 | 30 | A | 能力 |
| 6 | **实现完整度与稳定性** | **35** | **B** | **功能真实** |
| 7 | **规模与功能点** | **25** | A | **功能真实** |
| 8 | 代码规范性 | 10 | A | 文档/规范 |
| 9 | 演示与文档 | 8 | A | 文档/规范 |
| 10 | AI使用日志 | 10 | A | 过程 |
| 11 | **效果与数据** | **25** | **B** | **功能真实** |
| 12 | 安全性 | 9 | A | 文档/规范 |
**权重构成**(设计原则 2026-08-27):
- **功能真实类(85分,42.5%)**:实现完整度35 + 规模功能点25 + 效果数据25 —— 全部锚定确定性证据(构建/测试/功能发现/基准)。
- **Agent能力(30分)**:代码模式确定性检测 + AI 评分。
- **文档/规范类(27分,13.5%)**:代码规范10 + 演示文档8 + 安全9 —— 占比压低,且硬绑真实性(构建失败→半封顶)。
### 1.2 采分点速查
| 维度 | 采分点 | 关键规则 |
|:-----|:------|:--------|
| ① 场景价值(15) | 真实需求4·Agent不可替代4·ROI量化4·场景文档3 | 无文档→0;构建失败→≤4;**≤实现完整度** |
| ② 开发范式(8) | 流程覆盖3·设计文档3·测试文档2 | 构建失败→≤2 |
| ③ 架构设计(15) | 架构文档4·模块化4·数据流4·可扩展3 | 有文档+可构建→15;无文档有代码→≤8;构建失败→≤4;**≤实现完整度** |
| ④ 工具使用(10) | 框架集成6·工具链4 | 无框架0→基本2→MCP/FC4→深度6;构建失败→≤3 |
| ⑤ Agent核心(30) | 存在性5·工具调用8·规划6·协作4·可靠7 | **4门槛代码检测缺1→整维0**;构建失败→≤9 |
| ⑥ 实现完整度(35,B) | 功能完整12·构建8·启动7·降级5·错误3 | 功能发现锚定;**构建失败→≤11** |
| ⑦ 规模功能点(25) | 规模6·功能覆盖10·演示5·数据4 | 功能发现锚定;空壳>25%或测试弱→降档25% |
| ⑧ 代码规范(10) | 命名2·硬编码2·重复2·安全2·注释2 | 重复>30%→≤1、>50%→0 |
| ⑨ 演示文档(8) | README2·API文档2·启动说明2·一致性2 | 无README→0**一致性<60%→≤50%**;≤实现完整度 |
| ⑩ AI日志(10) | 记录4·调用细节4·真实性2 | **占位>50%→封顶30%**;声称无代码→扣2 |
| ⑪ 效果数据(25,B) | 测试6·框架4·验证数据5·覆盖率5·复现5 | **三档封顶**:无基准/测试→C档≤7;构建失败→≤7 |
| ⑫ 安全(9) | 密钥3·输入3·敏感2·依赖1 | 构建失败→≤2 |
**证据锚定来源**:功能发现轮(功能数/占位率)· tryBuild/tryStart/tryBrowse(构建/启动)· tryTest(测试/覆盖率)· accept-runner(验收命令)· ai-log-audit(日志占位)· evidence-detectAgent门槛)· claim-consistency(文档一致性)· code-quality(空壳/重复/测试有效度)
---
## 2. 赛道二:IDE+开发范式创新赛(8维 / 100分)
### 2.1 维度权重表
| # | 维度 | 分值 | 类型 |
|:-:|:-----|:----:|:----|
| 1 | 开发范式设计清晰度 | 20 | 设计 |
| 2 | IDE集成深度 | 20 | 能力 |
| 3 | 提效设计合理性 | 10 | 设计 |
| 4 | 提效幅度 | 10 | 效果 |
| 5 | 稳定性与易用性 | 15 | 功能真实 |
| 6 | 规模与功能点与技术难度 | 10 | 功能真实 |
| 7 | 演示与文档 | 5 | 文档 |
| 8 | AI使用日志 | 10 | 过程 |
### 2.2 采分点速查
| 维度 | 采分点 | 关键规则 |
|:-----|:------|:--------|
| ① 开发范式设计(20) | 范式定义·落地一致性·工具链结合 | 定性维度,不参与 C 档封顶 |
| ② IDE集成深度(20) | VSCode/IDEA插件·LSP·Webview·命令注册 | IDE 贡献点确定性提取注入 |
| ③ 提效设计合理(10) | 设计思路合理性 | 定性维度,不参与 C 档封顶 |
| ④ 提效幅度(10) | 基线对比数据 | **纯增益严格制**:无基线→C档≤30%;测试通过不构成提效证据 |
| ⑤ 稳定性易用(15) | 构建可运行·测试通过·可访问 | 确定性证据锚定 |
| ⑥ 规模功能点(10) | 真实功能数·占位比例 | 功能发现轮锚定 |
| ⑦ 演示文档(5) | 文档质量·一致性 | **一致性<60%→封顶50%** |
| ⑧ AI日志(10) | 记录·调用细节·真实性 | **占位>50%→封顶30%** |
> 单阶段评审(无 A/B 拆分);`extractIdeContributions` 提取 IDE 集成证据注入。
---
## 3. 确定性机制(真实性验证)
核心原则:**AI 负责评"品质",代码负责查"真假"**。以下机制全部由代码确定性计算,不依赖 AI 主观。
### 3.1 构建 / 测试 / 运行验证
| 机制 | 说明 | 封顶规则 |
|:-----|:-----|:--------|
| 人工确认构建 | 赛道一 A 后评委实际构建,verify 传 done/failed | 构建失败→实现完整度≤33%、效果≤30% |
| 自动 tryBuild | 7 种构建系统探测,作辅助证据 | 构建失败→纸面维度≤33% |
| tryTest 真跑 | pytest/jest/go 等,解析通过率+覆盖率 | 测试失败→效果≤50% |
| tryBrowse/tryStart | Web(45s看门狗)/CLI 服务验证 | — |
### 3.2 功能发现轮(Map-Reduce 全量扫描)
对全部源码分块扫描,AI 提取功能点并标注 `real/partial/stub`,统计**真实功能数 + 占位比例**,作为功能类维度评分锚点。防"AI 只看 README 不看真实代码"。
### 3.3 文档声称↔代码实现一致性(防补假文档)
AI 从 README 提取声称功能 → 与功能发现轮**语义比对** → 一致性率:
| 一致性率 | 判定 | 处理 |
|:-------:|:-----|:-----|
| ≥90% | 文档可信 | 正常给分 |
| 60~90% | 基本可信 | ×0.8 |
| <60% | **文档虚报** | 文档类维度封顶50% + 虚报项入评语 |
> 补**真实**文档→加分;补**编造**声称→降权。
### 3.4 代码质量评估(防空壳堆数量)
| 指标 | 算法 | 封顶规则 |
|:-----|:-----|:--------|
| 空壳率 | 空函数/占位 ÷ 总函数 | >25%→规模维度降档25% |
| 重复率 | 行级 MD5 去重 | >50%→代码规范≤3 |
| 测试有效度 | 断言数 ÷ (测试函数×2) | <40%→规模维度降档25% |
### 3.5 效果可验证三档
| 档位 | 条件 | 处理 |
|:---:|:-----|:-----|
| A | 有基准证据(benchmark) | 正常给分 |
| B | 测试通过或覆盖率非null | 正常给分 |
| C | 数据缺位 | **封顶30%** |
> **纯增益严格制**(提效幅度类):测试通过不构成提效证据,无基线一律 C 档。**定性维度豁免**(设计合理性/清晰度):不参与 C 档。
### 3.6 其他确定性校验
- **AI日志占位率审计**:占位>50%→AI日志维度封顶30%;声称技术须在代码找到。
- **Agent核心门槛**`evidence-detect` 代码模式匹配 4 项硬门槛(LLM调用/工具策略/重试降级/状态持久化),AI 只评品质要素。
- **测量协议 / 验收命令**:检查 `data/measurement/` 齐备性;执行 README `## 验收` 命令块。
---
## 4. 评分与可信度
### 4.1 校准(确定性)
LLM 只输出跨维度语义矛盾方向(over/under),**调幅由代码按统计规则计算**,不信任 LLM 数值:
| 级别 | 触发 | 调幅 |
|:---:|:-----|:----|
| L1 | 语义矛盾 | ±2 |
| L2 | σ 异常(偏离>2σ)| ±4 |
| L3 | 最不稳定维度(Agent核心/规模/效果)高估 | ×0.8 |
> 效果类维度 under 一律丢弃(只降不升);每次改动写入 `calibrationExplanation`(可审计)。
### 4.2 硬规则(Phase 3c,校准后执行)
| 规则 | 触发 | 封顶 |
|:-----|:-----|:-----|
| 构建失败 | buildFailed | 实现完整度≤33%、效果≤30%、纸面维度≤33% |
| 测试失败 | testStepFailed | 效果与数据≤50% |
| 重复代码>50% | duplicateRatio | 代码规范≤3 |
| 无根目录README | !hasRootReadme | 演示与文档≤2(无README)或≤3 |
| 文档虚报 | 一致性<60% | 演示与文档≤50% |
| 空壳率>25% | stubRatio | 规模维度降档25% |
| 测试有效度<40% | testValidity | 规模维度降档25% |
### 4.3 总分与迟交
- 校准 delta 用 `Math.round()` 取整。
- 总分 = 各维度分数累加(`finalScore = totalScore - 迟交扣分`,上限 `max_score_cap`)。
- 迟交判定:优先 git 最后 commit 时间;commit 缺失或早于条目创建 → 用条目创建时间兜底;`computeLateDays` 对比项目 deadline。
### 4.4 排名与聚合
- 排名用 `aggregateScores` / `aggregateEntryScores`:最近 N 次(默认3)快照 `score` 中位数。
- 聚合前提:各快照 `standard_snapshot` 一致。
- `<2 次标「初评(未达聚合样本)」`,排名区分正式/初评。
### 4.5 人机标定
-`docs/design/06-人机标定方案.md`
- 绝对准确未验证前,分数用于排名(相对序)可信;绝对解读需标定偏差基线。
---
## 5. 评审流程操作(评委视角)
### 5.1 赛道一(两阶段)
```
创建条目 → 启动评审(A) → A完成(status=a_done, 静态分)
→ 评委实际构建项目 → verify(done/failed)
→ B阶段(构建验证+实现类维度) → review_done → 2轮聚合
```
- **关键人工动作**:A 阶段后必须实际构建并确认 `done/failed`。Web 形态项目若未填 service_url,B 阶段降级"跳过浏览器测试"继续代码评审。
### 5.2 赛道二(单阶段)
```
创建条目 → 启动评审 → 全部维度一次完成 → review_done → 2轮聚合
```
---
## 6. 中期 + 最终评价(AI 评审 + 人工评审)
技术大赛分**中期评价**与**最终评价**两个阶段,成绩按 **2:8** 加权合成。发表会上队伍通过 PPT、视频、讲解说明项目,评委按维度打分表独立评分。
### 6.1 总分构成
| 阶段 | 权重 | 评审方式 |
|:-----|:---:|:---------|
| **中期评价** | 20% | **仅 AI 评审**(无人工)|
| **最终评价** | 80% | AI 评审 + 人工评审 |
**最终成绩 = 中期评价(20%)+ 最终评价(80%)**
- **中期评价(20%,仅 AI)**:基于 8/31 中期仓库快照,使用与最终评审同一套 AI 维度标准执行一次 AI 评审,不安排人工评审。
- **最终评价(80%,AI + 人工)**:AI 评审与人工评审两个独立评委,**AI 与人工总分一致**,成绩直接相加。
| 赛道 | AI 评审 | 人工评审 | 最终小计 |
|:----:|:------:|:------:|:------:|
| 赛道一 | 12维 / 200 | 8维 / 200 | 400 |
| 赛道二 | 8维 / 100 | 6维 / 100 | 200 |
**最终成绩 = 中期 AI(20%)+ 最终(AI + 人工)(80%)**。AI 与人工完全独立,人工不覆盖 AI 分。
### 6.2 赛道一人工维度(8维 / 200分)
| 维度 | 分值 | 考察点 |
|:-----|:----:|:-------|
| 现场真实性验证 | 20 | 视频/提交与代码一致(无现场演示,占比压低)|
| 演示完整性 | 30 | 核心功能展示到位、覆盖 README 声称 |
| AI 协作真实性 | 40 | 讲解的 AI 使用 vs 提交日志吻合;人主导+AI辅助 |
| AI 工具链理解 | 30 | Agent 架构/LLM/工具选择口头理解 |
| 答辩与技术深度 | 40 | 实现原理、架构讲解、质疑回应 |
| 演示与表达 | 20 | PPT、讲解、时间 |
| 创新与价值 | 10 | 场景价值、Agent 不可替代性 |
| 问题与改进意识 | 10 | 局限认知、改进计划 |
### 6.3 赛道二人工维度(6维 / 100分)
| 维度 | 分值 | 考察点 |
|:-----|:----:|:-------|
| 现场真实性验证 | 10 | 视频/提交与代码一致 |
| 演示完整性 | 20 | 核心集成场景展示到位 |
| 提效验证 | 30 | 提效幅度真实对比数据 |
| 答辩与技术深度 | 20 | IDE 集成技术原理、质疑回应 |
| 演示与表达 | 10 | PPT、讲解 |
| 创新与价值 | 10 | 场景价值、ROI |
> 中期评价不涉及人工评委;人工评审仅在最终评价(发表会)进行。5 名评委独立打分取平均。详细设计见 `docs/design/07-人工评审设计方案.md`。
---
## 7. 已知边界与注意
1. **评分用于相对排序更可信**:绝对分数受 AI 波动影响,多轮聚合取中位数缓解。
2. **确定性机制是"压制"而非"逐条证明"**:空壳率/一致性率等指标用于降档封顶,不逐条判定对错;重大存疑由人工复核。
3. **文档权重已刻意压低**:赛道一功能真实类占42.5%、文档类13.5%,防止"假文档拿高分"。
4. **评审结束删除 clone 目录**:只保留 review_snapshots(评分快照)与 feature_inventory(功能清单)。
5. **并发限制**MAX_CONCURRENT=3,第4个条目起排队。
---
*本文档由评审系统实现自动同步;如与 `server/config/standards/*.md` 标准文件冲突,以标准文件为准。*
+16
View File
@@ -373,6 +373,21 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚**
- 多步骤/多配置的功能(如企业标准A/B切换、检索策略对比),README 须提供分步命令清单,评审者将照此逐条执行
- 文档声称的功能须能在代码/运行中找到对应实现(「声称 vs 实测」交叉验证)
### 2.9.1 验收命令块(README 建议提供,2026-08-27 新增)
**建议在 README 末尾提供 `## 验收` 命令块**,给出评审系统可自动执行的一键验收命令(构建 → 测试 → 启动核心功能)。系统会在评审时**真实执行**并留存输出,作为「功能完整」相关维度确定性证据:
```markdown
## 验收
一键执行验收(构建 → 测试 → 启动核心功能):
\`\`\`bash
npm run build && npm test # 示例:按实际技术栈替换
\`\`\`
```
- 命令须无需人工干预可执行;无法一键验收的也要给出可执行命令。
-`## 验收` 块不强制扣分,但缺少该证据时「功能完整性」维度的验收证据将不完整。
---
## 2.10 提交前自查清单
@@ -393,6 +408,7 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚**
- [ ] 所有 LLM/API 调用处均已设置超时或异常防护
- [ ] 样本数据全部脱敏或虚构,无真实业务/客户数据
- [ ] 源码可按 README 步骤运行;已 push 且本地远端一致
- [ ] README 含 **`## 验收` 命令块**(建议提供一键验收命令)
---
@@ -283,6 +283,8 @@ A 阶段完成时写入**部分 ai_report**(含 A 维度 + scoreA),`a_done
| `entries` 表 | migration 新增 `score_a REAL DEFAULT 0`、`score_b REAL DEFAULT 0`、`stage_b_status TEXT DEFAULT ''`''=未执行 / done / failed / skipped)、`project_understanding TEXT DEFAULT ''`(§2.7 项目理解文档 JSON,A 每次重评覆盖) |
| `review_snapshots` | migration 新增 `score REAL`(含迟交扣分的 final_score,多次评审聚合用,2026-08-19 |
| `entries` 表 | migration 新增 `benchmark_json TEXT DEFAULT ''`(决赛圈基准证据按 entry 落库,2026-08-19 |
| `entries` 表 | migration 新增 `feature_inventory TEXT DEFAULT ''`(全量功能发现结果 JSON 数组,2026-08-27 |
| `ai_report.dimensions[n]` | 实现类维度子 Agent 返回的定向追踪 `checks` 数组(feature/entry/depth/evidence/reason2026-08-27 P1 修正) |
| `standards.max_score` | **不改**(仅上传校验上限,非实际满分,不影响出分) |
## 4. 影响面
@@ -0,0 +1,89 @@
# 07 人工评审设计方案
> 版本:2026-08-31
> 状态:设计定稿
> 关联:AI 评审机制(`docs/AI评审机制说明.md`
## 1. 背景与定位
技术大赛分**中期评价**与**最终评价**两个阶段,成绩按 **2:8** 加权合成。最终评价阶段采用 **AI 评审 + 人工评审双轨制**:发表会上各参赛队伍通过 PPT、视频、现场讲解对项目进行说明与演示,人工评审据此进行独立评分。中期评价仅由 AI 完成,不涉及人工评审。
**AI 与人工的定位**
| 评委 | 负责 | 无法覆盖 |
|:-----|:-----|:--------|
| AI 评审(独立评委)| 静态真伪:代码、构建、测试、功能发现、文档一致性 | 演示表达、口头答辩、专家判断 |
| 人工评审(独立评委)| 发表会表现:演示、答辩、创新价值、AI 过程可信度 | 全量代码审查 |
两者**完全独立评分**(人工不覆盖 AI 分、不设 AI 争议复核维),最终**直接相加**成总分。
## 2. 总分构成
| 阶段 | 权重 | 评审方式 |
|:-----|:---:|:---------|
| **中期评价** | 20% | **仅 AI 评审**(无人工)|
| **最终评价** | 80% | AI 评审 + 人工评审 |
**最终成绩 = 中期评价(20%)+ 最终评价(80%)**
| 赛道 | AI 评审 | 人工评审 | 最终小计 |
|:----:|:------:|:------:|:------:|
| 赛道一 | 12维 / 200 | 8维 / 200 | 400 |
| 赛道二 | 8维 / 100 | 6维 / 100 | 200 |
**最终小计 = AI 分 + 人工分**(直接相加)。**AI 与人工总分一致**(赛道一各 200、赛道二各 100)。人工评审由 5 名评委独立打分取平均。
## 3. 人工评审维度设计原则
1. **真实性维度占比压低**:发表会无现场演示环节(仅 PPT/视频/讲解),真实度不好评价——现场真实性维度占比两赛道均控制在 **10%**
2. **全维度分值 ≥5**:避免出现 2/3 分的琐碎维度,保证每个维度有实际考察分量。
3. **按赛道差异化**:赛道一聚焦 AI 协作过程(Agent 开发核心),赛道二聚焦提效验证(IDE 提效核心)。
4. **剔除不可靠评价项**:团队协作(无法从发表会证明)、产品体验(无法现场体验)不纳入。
## 4. 赛道一:人工评审(8 维 / 200 分)
| # | 维度 | 分值 | 采分点 |
|:-:|:-----|:----:|:-------|
| 1 | 现场真实性验证 | 20 | 视频/提交核验:功能展示与提交代码一致、数据可信 |
| 2 | 演示完整性 | 30 | 核心功能是否展示到位、覆盖 README 声称功能 |
| 3 | **AI 协作真实性** | 40 | 讲解的 AI 使用过程 vs 提交的 AI 日志吻合;人主导 + AI 辅助 |
| 4 | **AI 工具链理解** | 30 | 对 Agent 架构、LLM 调用、工具选择/降级的口头理解深度 |
| 5 | 答辩与技术深度 | 40 | 实现原理理解、架构讲解、对评委质疑的回应质量 |
| 6 | 演示与表达 | 20 | PPT 质量、讲解逻辑、语言清晰、时间控制 |
| 7 | 创新与价值 | 10 | 场景真实价值、Agent 不可替代性、技术先进性 |
| 8 | 问题与改进意识 | 10 | 主动讲局限、改进计划、对评审意见的接纳 |
| | **合计** | **200** | |
## 5. 赛道二:人工评审(6 维 / 100 分)
| # | 维度 | 分值 | 采分点 |
|:-:|:-----|:----:|:-------|
| 1 | 现场真实性验证 | 10 | 视频/提交核验:IDE 集成展示与提交一致、数据可信 |
| 2 | 演示完整性 | 20 | 核心集成场景是否展示到位 |
| 3 | **提效验证** | 30 | 提效幅度有真实对比数据支撑、基准可信 |
| 4 | 答辩与技术深度 | 20 | IDE 集成技术原理(LSP/Webview/命令等)、质疑回应 |
| 5 | 演示与表达 | 10 | PPT、讲解、时间控制 |
| 6 | 创新与价值 | 10 | 场景价值、提效价值、ROI |
| | **合计** | **100** | |
## 6. 赛道差异说明
| 差异点 | 赛道一 | 赛道二 |
|:------|:-------|:-------|
| 核心考察 | AI 协作过程真实性 | 提效验证 |
| 专属维度 | AI 协作真实性(40) + AI 工具链理解(30) | 提效验证(30) |
| 总分制 | 200(与 AI 一致)| 100(与 AI 一致)|
## 7. 实施要点(待后续实现)
- **评分录入**:发表会后评委按维度打分表录入(前端表单)。
- **数据落库**:新增人工评审记录表(entry_id、赛道、各维度分、评委、发表会日期)。
- **总分合成**:最终成绩 = 中期 AI(20%)+ 最终(AI + 人工)(80%);人工取 5 名评委平均分。
- **展示**:前端发表会评分页 + 详情页显示中期 AI 分、最终 AI 分/人工分、最终成绩。
- **PDF 报告**:评审报告增加中期评价与人工评审部分。
## 8. 已知边界
1. 人工评审的"现场真实性"因无现场演示,仅能核验视频/PPT 与提交一致性,**无法完全验证 demo 真伪**——故占比仅 10%。
2. 人工评分受评委主观性影响,采用 **5 名评委独立打分取平均**缓解。
3. 最终成绩由中期 AI(20%)+ 最终 AI/人工(80%)加权合成,AI 与人工在最终评价中占比相同;权重比例需在试运行后校准。
@@ -61,19 +61,13 @@ export interface FileSignature {
lang: 'ts' | 'js' | 'py' | 'other';
symbols: string[]; // export/function/class 定义行原文(≤5条)
}
export function buildInventory(files: {path,content}[]): {
signatures: FileSignature[]; // 全部文本文件
totalLines: number;
coreFiles: string[]; // 按规则选出的"精读文件包"
}
export function extractSignatures(files: {path,content}[]): FileSignature[]; // 全部文本文件
export function renderInventoryText(sigs: FileSignature[], budgetChars?: number): string; // 紧凑索引文本
```
- **语言范围**ts/tsx/js/jsx/mjs + pythondef/class 正则);其余语言仅记 path+lines
- **符号提取**:正则匹配 `^(export\s+)?(async\s+)?(function|class|const\s+\w+\s*=\s*(\(|async))` 与 python `^def |^class `
- **coreFiles 选择规则**(替代现"前 60 个文件"粗暴截断):
1. 入口/路由/服务/控制器目录下的代码文件(src/、app/、api/ 等)
2. DIM_FILE_FILTERS 各维度命中的文件
3. 按行数降序补足至预算
- **精读文件选择(coreFiles)**:实现中不单独产出 coreFiles 字段——精读包由 `filterFilesForDim` 的维度过滤(`DIM_FILE_FILTERS`)+ 40000 字符预算承担,**符号索引全量注入**所有实现类维度 prompt 头部,保证 100% 文件可见(2026-08-27 对齐实现,原 buildInventory/coreFiles 接口未落地,已废弃)
#### 3.1.2 投喂策略改造
@@ -92,7 +86,8 @@ export function buildInventory(files: {path,content}[]): {
[{feature:'一句话功能', files:['文件:行号'], depth:'real|partial|stub'}]"
合并去重 → FeatureInventory
{ features:[{name, files, depth}], totalFiles, analyzedChunks }
落库:entries.project_understanding 附加字段(或新列 feature_inventory TEXT
落库:entries.feature_inventory TEXTJSON 数组,2026-08-27 新增 migration),
同时返回文本注入实现类维度 prompt;落库失败非致命(不影响注入)
```
- 注入**功能完整性/规模·功能点**维度 prompt:「以下是全量代码扫描发现的实际功能清单,请对照 README 声称与验收基准逐项核对」
@@ -172,7 +167,7 @@ README/验收基准声称的核心功能如下:
真实=该项满分权重;部分=50%权重;占位=0分。
```
- **评分绑定**:子 Agent 返回的 checks 表存入 ai_report.dimensions[n].checks;解析失败回退现状(不崩)
- **评分绑定**:子 Agent 返回的 checks 表存入 ai_report.dimensions[n].checks2026-08-27 修复:runSubAgent 最终 prompt 对实现类维度要求 checks 字段,parseDimResponse 解析并透传,供详情页/PDF 展示);解析失败回退现状(不崩)
- 结合 3.3 信号:toPrompt 中的 stubFiles 作为"重点核查"提示一并注入
### 3.5 判档修正:定性设计维度豁免 C 档(解决问题 #4)
@@ -235,13 +230,18 @@ export function isQuantEvidenceDim(name): boolean // 替代 isEffectDim 在三
## 7. P1 实施清单
- [ ] code-inventory.ts 符号索引器 + 单测
- [ ] discoverFiles 投喂策略改造(索引全量 + coreFiles 全文 + 预算 40K
- [ ] discoverFeatureInventory Map-Reduce 功能发现轮 + 落库 + 注入
- [ ] ai-log-audit.ts + git 变更集采集 + 封顶应用 + 注入 + 单测
- [ ] code-signals.ts + 单测
- [ ] 实现类维度定向追踪 prompt 模板 + checks 解析 + 评分绑定
- [ ] standard-utils.ts 判档白名单重构(isQuantEvidenceDim+ 相关单测更新
- [ ] 分支提醒
- [ ] AGENTS.md 同步
- [ ] 回归:§6 四场景实测 + 全量 vitest
- [x] code-inventory.ts 符号索引器 + 单测
- [x] discoverFiles 投喂策略改造(索引全量 + coreFiles 全文 + 预算 40K
- [x] discoverFeatureInventory Map-Reduce 功能发现轮 + 落库feature_inventory 列)+ 注入
- [x] ai-log-audit.ts + git 变更集采集 + 封顶应用 + 注入 + 单测
- [x] code-signals.ts + 单测
- [x] 实现类维度定向追踪 prompt 模板 + checks 解析 + 评分绑定
- [x] standard-utils.ts 判档白名单重构(isQuantEvidenceDim+ 相关单测更新
- [x] 分支提醒
- [x] AGENTS.md 同步
- [x] 回归:§6 四场景实测 + 全量 vitest
> **2026-08-27 修正记录**P1 落地后审计发现 3 处文档-实现偏差,均已修正——
> ① §3.4 checks 表:原实现只注入 prompt 不解析存储,已修复(runSubAgent 最终 prompt 要求 checks + parseDimResponse 透传 + 5 项单测);
> ② §3.1.3 功能发现落库:原仅内存注入,已新增 entries.feature_inventory 列落库;
> ③ §3.1.1 coreFiles 接口:原设计 buildInventory/coreFiles 未落地,精读包由 filterFilesForDim + 40K 预算承担,文档已对齐实现。
+1 -1
View File
@@ -48,7 +48,7 @@
**验收标准:**
- US-04-1 带 track 创建项目 → 自动导入对应赛道默认标准
- US-04-2 赛道一/赛道二 标准维度数与总分符合模板(赛道一 12维/150、赛道二 8维/100
- US-04-2 赛道一/赛道二 标准维度数与总分符合模板(赛道一 12维/200、赛道二 8维/100
### US-05 选择官方选题的参赛者(A1-A6/B1-B6
**作为** 一名选择官方给定选题的参赛者,
@@ -137,6 +137,39 @@
> **项目理解文档(评审必读)**:评审系统会读取仓库内文档与源码自动生成"项目理解文档"。因此代码与文档应真实反映实际实现,**文档声称的功能须能在代码/运行中找到对应实现**(存在"声称 vs 实测"交叉验证,不符将扣分)。
### 7.1 验收命令块(README 必填,2026-08-27 新增)
**README.md 末尾须提供 `## 验收` 命令块**,给出评审系统可自动执行的验收命令(一键构建 + 跑测试 + 启动,验证"声称的核心功能真实可用")。系统会在评审时**真实执行**该命令并留存输出:
```markdown
## 验收
一键执行验收(构建 → 测试 → 启动核心功能):
\`\`\`bash
npm run build && npm test # 示例:按实际技术栈替换
\`\`\`
```
要求:
- 命令须**无需人工干预可执行**(不依赖交互式输入、不依赖私有网络)。
- 验收命令输出应能证明"核心功能跑通"(如测试通过汇总、服务启动日志)。
- 无法一键验收的项目(如纯静态演示)也要给出可执行命令;**无 `## 验收` 块的项目,评审时该证据缺失,相关维度(实现完整度/效果与数据)无法获得验收证据分**。
### 7.2 测量协议(data/measurement/,提效/效果类作品必填,2026-08-27 新增)
**申报提效/效果数据的作品**(如"提效幅度 X%")须在仓库内提交**可复现的测量协议**,供评审核查数据真实性:
```
data/measurement/
├── baseline/ # 改造前基线:测量脚本 + 原始输出
├── README.md # 测量说明:环境、步骤、指标口径
└── timing-log.* # 逐次测量记录(时间戳 + 指标值)
```
- 无基线的提效声明:评审按"数据缺位(未证明)"处理,提效/效果类维度封顶(详见评审标准)。
- 测量记录应包含:测量日期、运行环境、重复次数、单次结果与汇总。
> 上述 `## 验收` 命令块与测量协议均系**确定性证据**——系统真实执行并留存输出,AI 评分必须据此,不得忽略或低估。
---
## 8. 成果物清单与放置规则
@@ -201,6 +234,7 @@
- [ ] `_AI_USAGE_LOG.md` 字段完整,范式步骤与范式图一致
- [ ] 文件名无空格/中文/特殊字符;无密钥/token/.env/构建产物
- [ ] 源码可按 README 启动运行;已 `git push`,本地与远端一致
- [ ] README.md 含 **`## 验收` 命令块**(一键构建+测试+启动);申报提效数据的提交 `data/measurement/` 测量协议
## 11. 违规后果
@@ -134,6 +134,39 @@
> **项目理解文档(评审必读)**:评审系统会读取仓库内文档与源码自动生成"项目理解文档"。因此代码与文档应真实反映实际实现,**文档声称的功能须能在代码/运行中找到对应实现**(存在"声称 vs 实测"交叉验证,不符将扣分)。
### 7.1 验收命令块(README 必填,2026-08-27 新增)
**README.md 末尾须提供 `## 验收` 命令块**,给出评审系统可自动执行的验收命令(一键构建 + 跑测试 + 启动,验证"声称的核心功能真实可用")。系统会在评审时**真实执行**该命令并留存输出:
```markdown
## 验收
一键执行验收(构建 → 测试 → 启动核心功能):
\`\`\`bash
npm run build && npm test # 示例:按实际技术栈替换
\`\`\`
```
要求:
- 命令须**无需人工干预可执行**(不依赖交互式输入、不依赖私有网络)。
- 验收命令输出应能证明"核心功能跑通"(如测试通过汇总、服务启动日志)。
- 无法一键验收的项目(如纯静态演示)也要给出可执行命令;**无 `## 验收` 块的项目,评审时该证据缺失,相关维度(实现完整度/效果与数据)无法获得验收证据分**。
### 7.2 测量协议(data/measurement/,提效/效果类作品必填,2026-08-27 新增)
**申报提效/效果数据的作品**(如"提效幅度 X%")须在仓库内提交**可复现的测量协议**,供评审核查数据真实性:
```
data/measurement/
├── baseline/ # 改造前基线:测量脚本 + 原始输出
├── README.md # 测量说明:环境、步骤、指标口径
└── timing-log.* # 逐次测量记录(时间戳 + 指标值)
```
- 无基线的提效声明:评审按"数据缺位(未证明)"处理,提效/效果类维度封顶(详见评审标准)。
- 测量记录应包含:测量日期、运行环境、重复次数、单次结果与汇总。
> 上述 `## 验收` 命令块与测量协议均系**确定性证据**——系统真实执行并留存输出,AI 评分必须据此,不得忽略或低估。
---
## 8. 成果物清单与放置规则
@@ -198,6 +231,7 @@
- [ ] `_AI_USAGE_LOG.md` 字段完整,范式步骤与范式图一致
- [ ] 文件名无空格/中文/特殊字符;无密钥/token/.env/构建产物
- [ ] 源码可按 README 启动运行;已 `git push`,本地与远端一致
- [ ] README.md 含 **`## 验收` 命令块**(一键构建+测试+启动);申报提效数据的提交 `data/measurement/` 测量协议
## 11. 违规后果
+79
View File
@@ -0,0 +1,79 @@
# 评审表(评委打分用)
> 版本:2026-08-31
> 用途:发表会人工评审打分(最终评价)
> 说明:评委按档位打分(优秀 85-100% / 合格 50-84% / 不足 <50%),**5 名评委独立打分取平均分**。赛道一人工总分 200(与 AI 评审一致)、赛道二人工总分 100(与 AI 评审一致)。
**队伍信息**____________ **赛道**:□ 赛道一  □ 赛道二 **评委**____________ **日期**____________
---
## 赛道一 · 人工评审表(8 维 / 200 分)
| # | 维度 | 分值 | 打分区间 | 考察点 | 得分 |
|:-:|:-----|:----:|:---------|:-------|:----:|
| 1 | 现场真实性验证 | 20 | 优秀 17-20 / 合格 10-16 / 不足 <10 | 演示与代码一致、数据可信 | ____ |
| 2 | 演示完整性 | 30 | 优秀 25.5-30 / 合格 15-25.4 / 不足 <15 | 核心功能展示到位、覆盖 README 声称 | ____ |
| 3 | AI 协作真实性 | 40 | 优秀 34-40 / 合格 20-33 / 不足 <20 | 讲解 vs AI 日志吻合、人主导+AI辅助 | ____ |
| 4 | AI 工具链理解 | 30 | 优秀 25.5-30 / 合格 15-25.4 / 不足 <15 | Agent 架构/LLM/工具选择/降级 | ____ |
| 5 | 答辩与技术深度 | 40 | 优秀 34-40 / 合格 20-33 / 不足 <20 | 实现原理、架构讲解、质疑回应 | ____ |
| 6 | 演示与表达 | 20 | 优秀 17-20 / 合格 10-16 / 不足 <10 | PPT 质量、讲解、时间控制 | ____ |
| 7 | 创新与价值 | 10 | 优秀 8.5-10 / 合格 5-8.4 / 不足 <5 | 场景价值、Agent 不可替代性 | ____ |
| 8 | 问题与改进意识 | 10 | 优秀 8.5-10 / 合格 5-8.4 / 不足 <5 | 局限认知、改进计划 | ____ |
| | **合计** | **200** | | | **____** |
---
## 赛道二 · 人工评审表(6 维 / 100 分)
| # | 维度 | 分值 | 打分区间 | 考察点 | 得分 |
|:-:|:-----|:----:|:---------|:-------|:----:|
| 1 | 现场真实性验证 | 10 | 优秀 8.5-10 / 合格 5-8.4 / 不足 <5 | 演示与代码一致、数据可信 | ____ |
| 2 | 演示完整性 | 20 | 优秀 17-20 / 合格 10-16 / 不足 <10 | IDE 集成核心场景演示到位 | ____ |
| 3 | 提效验证 | 30 | 优秀 25.5-30 / 合格 15-25.4 / 不足 <15 | 提效对比数据真实、基准可信 | ____ |
| 4 | 答辩与技术深度 | 20 | 优秀 17-20 / 合格 10-16 / 不足 <10 | IDE 集成技术理解、质疑回应 | ____ |
| 5 | 演示与表达 | 10 | 优秀 8.5-10 / 合格 5-8.4 / 不足 <5 | PPT 质量、讲解 | ____ |
| 6 | 创新与价值 | 10 | 优秀 8.5-10 / 合格 5-8.4 / 不足 <5 | 场景价值、提效价值、ROI | ____ |
| | **合计** | **100** | | | **____** |
---
## 评委参考问题(答辩提问)
### 通用(两赛道)
| 维度 | 参考问题 |
|:-----|:---------|
| 现场真实性 | "这个功能对应的代码在哪?演示数据怎么生成的、能复现吗?视频和提交是同一版本吗?" |
| 演示完整性 | "README 声称的 X 功能演示一下?核心业务闭环完整演示了吗?" |
| 答辩与技术 | "这个技术选型为什么这么选?模块边界/异常怎么处理?场景变化能支撑吗?" |
| 演示与表达 | (观察 PPT 质量、讲解逻辑、时间控制) |
| 创新与价值 | "为什么非这么做不可?传统方法解决不了吗?实际用在哪?" |
### 赛道一专属
| 维度 | 参考问题 |
|:-----|:---------|
| AI 协作真实性 | "哪个环节用了 AI?用了哪些模型?日志这条记录对应哪个改动?人写 vs AI 生成怎么分工?怎么把控质量?" |
| AI 工具链理解 | "Agent 怎么决定调用哪个工具?LLM 失败怎么降级?多步任务怎么规划执行?" |
### 赛道二专属
| 维度 | 参考问题 |
|:-----|:---------|
| 提效验证 | "改造前基线怎么测的?前后环境一致吗?测了几次取什么值?提效 X% 怎么算的?" |
---
## 评分备注
- **造假标记**:若发现演示与提交明显不符/数据伪造,在下方备注并上报组委会。
- **AI 分复核建议**:如认为 AI 分与该队实际表现差异大,备注"建议复核"(不直接改 AI 分)。
- **中期评价不涉及本表**:中期评价(20%)仅由 AI 完成,本表仅用于最终评价(80%)的人工评审部分;最终成绩 = 中期 AI(20%)+ 最终(AI + 人工)(80%)。
- **5 名评委独立打分**,各队人工评审成绩取 5 人平均分。
**备注**____________________________________________________________
---
*配套文档:《赛事说明》《评审规则详细说明》。*
+313
View File
@@ -0,0 +1,313 @@
# 评审规则详细说明
> 版本:2026-08-31
> 适用:技术大赛 赛道一 / 赛道二 评审
> 读者:评委(重点)、组委会
本文档详细说明评审规则:**中期评价(仅 AI)+ 最终评价(AI + 人工)** 的整体结构、AI 评审标准概览 + **人工评审(发表会)的详细评分指南**,帮助评委在发表会上准确、一致地评价各参赛队。
---
## 1. 评审体系总览
技术大赛分**中期评价**与**最终评价**两个阶段,成绩按 **2:8** 加权合成:
| 阶段 | 权重 | 评审方式 | 依据 |
|:-----|:---:|:---------|:-----|
| **中期评价** | 20% | **仅 AI 评审**(无人工)| 8/31 中期仓库快照,AI 静态评审 |
| **最终评价** | 80% | AI 评审 + 人工评审 | AI 拉仓库静态评审 + 发表会现场人工打分 |
**最终成绩 = 中期评价(20%)+ 最终评价(80%)**
- **中期评价(20%,仅 AI)**:基于 8/31 中期提交的仓库快照,由系统自动执行一次 AI 评审,只评已完成部分。**不安排人工评审**。
- **最终评价(80%,AI + 人工)**:AI 评审(静态全面审查)与人工评审(发表会现场评价)两个评委构成,**AI 与人工总分一致**,成绩直接相加。
**评分构成**
- 赛道一:AI 12 维 / 200 + 人工 8 维 / 200 = 最终 400;中期 20% + 最终 80% 加权合成
- 赛道二:AI 8 维 / 100 + 人工 6 维 / 100 = 最终 200;中期 20% + 最终 80% 加权合成
> 中期仅 AI 完成;人工评审只在最终评价(发表会)进行。两轨完全独立:人工评审不覆盖、不修改 AI 分。
---
## 2. AI 评审标准(概览)
AI 评审由系统自动完成,评委主要了解其评什么、结果如何解读。详细采分机制见《AI评审机制说明》。
> **中期评价**:使用与最终评审**同一套 AI 维度标准**,对 8/31 中期快照执行一次 AI 评审,作为最终成绩的 20% 权重。中期不涉及人工评委,其评分说明由系统自动生成。
### 2.1 赛道一:AI 维度(12 维 / 200 分)
| 维度 | 分值 | 一句话标准 |
|:-----|:----:|:----------|
| 场景价值与技术合理性 | 15 | 真实业务问题、Agent 不可替代 |
| 开发范式应用 | 8 | AI 开发流程真实性 |
| 架构设计 | 15 | 架构文档与代码质量 |
| 工具使用与Skill集成深度 | 10 | AI 框架与工具链 |
| Agent核心能力 | 30 | LLM 调用/工具/重试/状态 |
| 实现完整度与稳定性 | 35 | 功能真实、构建可运行 |
| 规模与功能点 | 25 | 真实功能数与占位 |
| 代码规范性 | 10 | 命名/硬编码/重复 |
| 演示与文档 | 8 | 文档质量与一致性 |
| AI使用日志 | 10 | 日志真实性与占位率 |
| 效果与数据 | 25 | 测试/覆盖率/对比数据 |
| 安全性 | 9 | 密钥/输入/敏感信息 |
### 2.2 赛道二:AI 维度(8 维 / 100 分)
| 维度 | 分值 | 一句话标准 |
|:-----|:----:|:----------|
| 开发范式设计清晰度 | 20 | 提效范式定义与落地 |
| IDE集成深度 | 20 | IDE 真实集成能力 |
| 提效设计合理性 | 10 | 提效方案设计思路 |
| 提效幅度 | 10 | 真实对比数据支撑 |
| 稳定性与易用性 | 15 | 构建可运行、易用 |
| 规模与功能点与技术难度 | 10 | 功能数与技术难度 |
| 演示与文档 | 5 | 文档质量与一致性 |
| AI使用日志 | 10 | 日志真实性与占位率 |
### 2.3 AI 评审的确定性机制(要点)
AI 评审的"真假判定"全部由系统确定性计算,不靠 AI 主观:
- **构建/测试验证**:真实构建、真跑测试,解析通过率与覆盖率。
- **功能发现轮**:全量扫描源码,统计真实/部分/占位功能。
- **文档一致性**:文档声称的功能 ↔ 代码真实实现交叉比对。
- **质量评估**:空壳率、重复率、测试有效度。
- **硬规则封顶**:构建失败、测试失败、文档虚报 → 相关维度封顶。
> 评委无需理解全部机制细节,重点是:**AI 分反映的是"静态代码质量",人工分反映"发表会表现"**,两者互补。
---
## 3. 人工评审(发表会)
### 3.1 发表会流程
| 环节 | 时长 | 内容 |
|:-----|:----:|:-----|
| PPT 讲解 | 约 5-8 分钟 | 团队、主题、解决的问题、方案说明 |
| 视频演示 | ≤10 分钟 | 项目主要功能演示 |
| 答辩提问 | 约 5-10 分钟 | 评委按评审表维度提问 |
**赛道一追加**PPT 须说明 **Agent 的作用**(AI 角色、如何工作、Agent 闭环)。
### 3.2 评分方式
- 每位评委按评审表对每条目逐维度打分。
- 打分采用**三档制**:评委先判断档位(优秀/合格/不足),再在该档位区间内给具体分。
- **5 名评委独立打分,取平均分**作为该队人工评审成绩。
- 打分区间(占该维度满分比例):
- **优秀(85~100%)**:表现突出,明显超出基本要求
- **合格(50~84%)**:达到基本要求,无明显硬伤
- **不足(<50%)**:明显缺失或无法展示
---
### 3.3 赛道一人工评审评分指南(8 维 / 200 分)
#### 维度 1:现场真实性验证(20 分)
**考察**:视频/讲解展示的功能与提交代码是否一致,数据是否可信。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 演示功能与代码完全对应,数据可追溯,讲解与提交一致 |
| 合格 | 主要功能一致,个别细节模糊 |
| 不足 | 演示与提交明显不符,或关键功能无法展示 |
**评委参考问题**
- "请说明这个功能对应的代码位置?"
- "这个演示数据是怎么生成的?能复现吗?"
- "视频里演示的和仓库里提交的是同一版本吗?"
#### 维度 2:演示完整性(30 分)
**考察**:核心功能是否展示到位,是否覆盖 README 声称的功能。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 核心功能全部演示,覆盖 README 声称,演示路径完整 |
| 合格 | 主要功能演示,个别次要功能未覆盖 |
| 不足 | 仅演示少量功能,或与声称功能差距大 |
**评委参考问题**
- "README 声称的 X 功能能演示一下吗?"
- "你们的核心业务闭环完整演示了吗?"
#### 维度 3:AI 协作真实性(40 分)
**考察**:讲解的 AI 使用过程 ↔ 提交的 AI 日志是否吻合;人主导 + AI 辅助。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | AI 使用过程讲得具体可信,与日志逐条吻合,人机分工清晰 |
| 合格 | 大致吻合,细节不完整 |
| 不足 | 讲解与日志明显不符,或说不清 AI 怎么用的 |
**评委参考问题**
- "你们在哪个环节用了 AI?用了哪些模型?"
- "AI 日志里这条记录对应哪个改动?"
- "哪些部分是人写的、哪些是 AI 生成的?怎么把控质量?"
#### 维度 4:AI 工具链理解(30 分)
**考察**:对 Agent 架构、LLM 调用、工具选择/降级机制的口头理解。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 能清晰讲清 Agent 架构、工具调用、异常降级原理 |
| 合格 | 能基本讲清主要机制 |
| 不足 | 说不清 Agent 怎么工作,或只是"套用" |
**评委参考问题**
- "你的 Agent 怎么决定调用哪个工具?"
- "LLM 调用失败时怎么处理?有降级吗?"
- "多步任务怎么规划和执行的?"
#### 维度 5:答辩与技术深度(40 分)
**考察**:实现原理、架构讲解、对质疑的回应质量。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 技术细节掌握扎实,能深入讲解原理,回应质疑有理有据 |
| 合格 | 能讲解主要实现,回应基本到位 |
| 不足 | 说不清实现原理,回避问题或答非所问 |
**评委参考问题**
- "这个技术选型为什么这么选?有对比吗?"
- "这个模块的边界/异常情况怎么处理?"
- "如果数据量增大/场景变化,你的方案能支撑吗?"
#### 维度 6:演示与表达(20 分)
**考察**:PPT 质量、讲解逻辑、语言清晰、时间控制。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | PPT 专业、讲解流畅有感染力、时间控制好 |
| 合格 | 表达清楚,个别环节平淡 |
| 不足 | PPT 混乱、讲解不清或严重超时 |
#### 维度 7:创新与价值(10 分)
**考察**:场景真实价值、Agent 不可替代性、技术先进性。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 解决真实痛点,Agent 不可替代性明确,有创新 |
| 合格 | 有价值但创新性或必要性一般 |
| 不足 | 场景价值弱,或用传统方法也能做 |
**评委参考问题**
- "为什么非用 Agent 不可?传统脚本/工具解决不了吗?"
- "这个场景真实存在吗?实际用在哪?"
#### 维度 8:问题与改进意识(10 分)
**考察**:主动讲局限、改进计划、对评审意见的接纳。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 主动讲清局限与改进计划,接纳意见 |
| 合格 | 能回应但不够主动 |
| 不足 | 回避不足或拒不接纳 |
---
### 3.4 赛道二人工评审评分指南(6 维 / 100 分)
#### 维度 1:现场真实性验证(10 分)
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 演示与代码一致、数据可信可复现 |
| 合格 | 主要一致,细节模糊 |
| 不足 | 明显不符或无法展示 |
**参考问题**:"提效数据怎么测的?能现场复现吗?"
#### 维度 2:演示完整性(20 分)
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | IDE 集成核心场景完整演示 |
| 合格 | 主要场景演示 |
| 不足 | 少量演示或演示失败 |
#### 维度 3:提效验证(30 分)
**考察**:提效幅度有真实对比数据支撑、基准可信。
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 有清晰的改造前/后对比数据,测量方法可信,提效幅度有说服力 |
| 合格 | 有对比数据但方法或样本不够严谨 |
| 不足 | 只有口头声称,无数据或数据不可信 |
**评委参考问题**
- "改造前基线是怎么测的?和改造后环境一致吗?"
- "测了几次?平均还是最优值?"
- "提效 XX% 是怎么算出来的?"
#### 维度 4:答辩与技术深度(20 分)
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 对 IDE 集成技术(LSP/Webview/命令等)理解深入 |
| 合格 | 能讲清主要实现 |
| 不足 | 说不清实现原理 |
#### 维度 5:演示与表达(10 分)
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | PPT 专业、讲解清晰 |
| 合格 | 表达清楚 |
| 不足 | 表达混乱 |
#### 维度 6:创新与价值(10 分)
| 档位 | 表现描述 |
|:----|:---------|
| 优秀 | 场景真实、提效价值明确、有先进性 |
| 合格 | 有价值但一般 |
| 不足 | 价值弱 |
---
## 4. 评分注意事项
### 4.1 真实性是硬底线
- 现场真实性维度虽然分值占比低(10%),但**若发现明显造假**(演示与提交不符、数据伪造),评委应在评分表备注中标注,并上报组委会。
- 提效/效果数据必须追问**测量方法**,防"编数据"。
### 4.2 三档制的使用
- 先整体判断档位,再在档位区间给分,避免随意给分。
- 档位区间:优秀 85-100%、合格 50-84%、不足 <50%。
- 例:赛道一维度 3(AI 协作真实性,满分 40),优秀=34-40、合格=20-33、不足=<20。
### 4.3 多评委一致性
- 5 名评委各自独立打分,不互相讨论后统一给分。
- 最终**取 5 人平均分**作为人工评审成绩。
- 分差过大的维度(评委间差 >40%)提交组委会复核。
### 4.4 与 AI 分的关系
- 人工评审**只看发表会表现**,不因 AI 分高低调整人工分。
- 评委如认为 AI 分与该队实际表现差异大,可备注"建议复核",由组委会处理,不直接改分。
- **中期评价仅由 AI 完成**,评委不参与;最终成绩 = 中期 AI(20%)+ 最终(AI + 人工)(80%)。
---
## 5. 评审表
评委在发表会使用《评审表》打分(附维度、分值、打分区间、参考问题摘要)。见配套文档《评审表》。
---
*本文档与《赛事说明》《评审表》配套使用。*
+194
View File
@@ -0,0 +1,194 @@
# 赛事说明
> 版本:2026-08-31
> 适用范围:技术大赛 赛道一(Agent开发实战赛)/ 赛道二(IDE+开发范式创新赛)
> 读者:参赛选手、评委、组委会
本文档说明技术大赛的**赛程计划、整体评审规则、主要特点与维度总览**。成果物提交流程详见《参赛成果物提交规范-赛道一/赛道二》;评审的详细采分规则见《评审规则详细说明》。
---
## 1. 赛事概况
| 项目 | 赛道一 | 赛道二 |
|:-----|:-------|:-------|
| 名称 | Agent开发实战赛 | IDE+开发范式创新赛 |
| 主题 | 用 AI Agent 解决实际业务问题 | 打造 IDE 集成 / 提效工具 |
| 参赛规模 | 9 队 | 5 队 |
| 评审方式 | AI 评审 + 人工评审(发表会)双轨制 | 同左 |
| 评价结构 | 中期评价(仅 AI)+ 最终评价(AI + 人工) | 同左 |
---
## 2. 赛程计划
| 阶段 | 赛道一 | 赛道二 |
|:-----|:-------|:-------|
| 中期提交截止 | **8/31(周一)** | **8/31(周一)** |
| 组委会反馈采集情况 | 9 月首周 | 9 月首周 |
| **最终提交截止** | **10/31** | **9/15** |
| **发表会(即评审)** | **11 月中** | **9 月底** |
| 评审结果公开 | 11 月 | 9 月底 |
### 2.1 中期提交(两赛道相同)
- **截止**:8/31(周一)前提交中期成果至 Gitea。
- **内容**:成果物清单(§3)中任选部分即可,不要求全部;将已完成部分提交即可。
- **方式**`git push` 到 main 分支,确保服务器能采集。
- **反馈**:8/31 后约 1 周内(9 月首周),组委会反馈各队成果物的采集情况。
> **中期评价**:组委会将基于中期仓库快照执行一次 AI 评审(仅 AI,无人工),其结果按 §4.1 计入最终成绩。
### 2.2 最终提交与发表会
| 事项 | 赛道一 | 赛道二 |
|:-----|:-------|:-------|
| 最终提交截止 | 10/31 | 9/15 |
| 提交内容 | 最终成果物(部分或全部)| **必须提交全部成果物(01-06** |
| 发表会 | 11 月中 | 9 月底 |
| 发表材料 | PPT + 视频 ≤10 分钟 + 讲解 | PPT + 视频 ≤10 分钟 + 讲解 |
**发表会材料要求**
- **PPT≤10 分钟**:介绍团队、项目主题、解决的课题、提效说明等。
- **视频 ≤10 分钟**:对项目主要功能进行演示。
- **赛道一追加**PPT 中须**说明 Agent 的作用**(AI 在项目中扮演什么角色、如何工作、Agent 闭环)。
- 发表会现场评委按评审表打分,作为**人工评审**成绩。
> **状态锁定**:最终提交截止后系统锁定该队状态,进入评审流程。
---
## 3. 成果物提交(详细要求见《参赛成果物提交规范-赛道一 / 赛道二》)
### 3.1 提交方式
- 各队使用组委会下发的 Gitea 独立账号,提交到统一仓库 `2026Technology-Competition`main 分支)。
- 账号密码**请勿修改**(评审系统依赖固定凭据拉取)。
- 提交后验证服务器能正常拉取;无法采集及时联系组委会。
### 3.2 成果物清单(01-06
| # | 成果物 | 放置位置 | 内容要点 |
|:-:|:------|:--------|:--------|
| 01 | 项目说明 | 根目录 `README.md` | 项目性质声明、概述、功能说明、效果总结、团队分工 |
| 02 | 设计文档 | 根目录 `DESIGN.md``docs/` | 场景与价值、开发范式流程图、架构图 |
| 03 | 源码 | 根目录或 `src/` | 可运行源码,安装/运行方法写 README |
| 04 | 实验报告 | `tests/` + `coverage/` | 测试用例、执行结果、覆盖率报告 |
| 05 | AI 使用日志 | 根目录 `_AI_USAGE_LOG.md` | 覆盖需求/设计/编码/测试全环节 |
| 06 | 演示视频 | `docs/demo.mp4` 或根目录 | 赛道一 ≤15 分钟、赛道二 ≤5 分钟(仓库内视频)|
### 3.3 强制要求(摘要)
- **README 必须为根目录文件**;源码须能按 README 启动运行(基础前提)。
- README 末尾须含 **`## 验收` 命令块**(一键构建+测试+启动,供系统真实执行)。
- 申报提效/效果数据须提交 **`data/measurement/`** 测量协议(基线+测量记录)。
- 禁止提交密钥/token/.env/构建产物;文件名用 ASCII,禁止空格与中文。
---
## 4. 评审方式:中期(仅 AI)+ 最终(AI + 人工)双轨制
技术大赛分**中期评价**与**最终评价**两个阶段,成绩按 **2:8** 加权合成。
### 4.1 评分构成
| 阶段 | 权重 | 评审方式 |
|:-----|:---:|:---------|
| **中期评价** | 20% | **仅 AI 评审**(无人工)|
| **最终评价** | 80% | AI 评审 + 人工评审 |
**最终成绩 = 中期评价(20%)+ 最终评价(80%)**
- **中期评价(20%,仅 AI)**:基于 8/31 中期仓库快照,由 AI 静态评审已完成部分成果物。
- **最终评价(80%,AI + 人工)**:AI 评审(静态全面审查)与人工评审(发表会现场评价)两个评委构成,**AI 与人工总分相同**,成绩直接相加。
| 评审轨 | 评审什么 | 依据 |
|:-------|:---------|:-----|
| **AI 评审** | 代码、构建、测试、功能真实性、文档一致性、Agent 能力等 | 自动拉取仓库,确定性证据 + AI 评分 |
| **人工评审** | 发表会表现:PPT、视频演示、讲解、答辩、创新价值等 | 评委现场按评审表打分 |
> **说明**:中期评价只由 AI 完成、不安排人工评审;人工评审仅在最终评价(发表会)进行。AI 与人工评审各自独立评分,总分一致。
---
## 5. 评审主要特点
| 特点 | 说明 |
|:-----|:-----|
| 🎯 **真实性优先** | 评分以"系统能否真实构建、功能是否真实现、测试是否真通过"为硬依据,而非文档写得漂亮 |
| 🛡 **AI 不判真假** | 构建结果、测试通过率、功能占位率、日志占位率、文档一致性由系统**确定性判定**,AI 只评品质 |
| 🚫 **防作弊** | 文档声称↔代码实现一致性校验;空壳率/重复率/测试有效度质量评估。补真→加分,编造→降权 |
| 📊 **结果稳定** | 最近 3 次 AI 评审取中位数(2 次取平均);标准快照隔离 |
| 🔍 **可审计** | 每维度分带评语+证据引用;历次评审可对比 |
| 👥 **双轨制** | 中期仅 AI 评审 + 最终 AI/人工双评审;AI 与人工总分一致 |
---
## 6. 评审维度总览
### 6.1 赛道一:AI 评审维度(12 维 / 200 分)
| # | 维度 | 分值 | 一句话介绍 |
|:-:|:-----|:----:|:----------|
| 1 | 场景价值与技术合理性 | 15 | 解决真实业务问题,Agent 不可替代 |
| 2 | 开发范式应用 | 8 | AI 开发流程是否真实覆盖需求→设计→编码→测试 |
| 3 | 架构设计 | 15 | 架构文档与代码分层、模块化、可扩展 |
| 4 | 工具使用与Skill集成深度 | 10 | AI 框架、MCP/Function Calling、IDE 集成 |
| 5 | Agent核心能力 | 30 | LLM 调用、工具选择、重试降级、状态持久化 |
| 6 | 实现完整度与稳定性 | 35 | 功能真实实现、构建可运行、服务可启动 |
| 7 | 规模与功能点 | 25 | 真实功能数量与占位比例 |
| 8 | 代码规范性 | 10 | 命名、硬编码、重复代码、安全规范 |
| 9 | 演示与文档 | 8 | README/文档质量与一致性 |
| 10 | AI使用日志 | 10 | 日志真实性、占位率、过程可溯源 |
| 11 | 效果与数据 | 25 | 测试通过、覆盖率、效果对比数据 |
| 12 | 安全性 | 9 | 密钥管理、输入验证、敏感信息保护 |
### 6.2 赛道二:AI 评审维度(8 维 / 100 分)
| # | 维度 | 分值 | 一句话介绍 |
|:-:|:-----|:----:|:----------|
| 1 | 开发范式设计清晰度 | 20 | 提效范式定义与落地的清晰度 |
| 2 | IDE集成深度 | 20 | 插件/工具在 IDE 中的真实集成能力 |
| 3 | 提效设计合理性 | 10 | 提效方案设计思路的合理性 |
| 4 | 提效幅度 | 10 | 提效幅度有真实对比数据支撑 |
| 5 | 稳定性与易用性 | 15 | 构建可运行、测试通过、易用 |
| 6 | 规模与功能点与技术难度 | 10 | 真实功能数量与技术难度 |
| 7 | 演示与文档 | 5 | 文档质量与一致性 |
| 8 | AI使用日志 | 10 | 日志真实性、占位率、过程可溯源 |
### 6.3 人工评审维度(发表会)
> 人工评审总分与 AI 评审总分一致(**赛道一 200 分 / 赛道二 100 分**),细分维度分值构成详见《评审表》。
| 维度 | 赛道一 | 赛道二 |
|:-----|:------:|:------:|
| 现场真实性验证 | ● | ● |
| 演示完整性 | ● | ● |
| AI 协作真实性 / 提效验证 | ● | ● |
| AI 工具链理解 / 答辩与技术深度 | ● | ● |
| 答辩与技术深度 / 演示与表达 | ● | ● |
| 演示与表达 / 创新与价值 | ● | ● |
| 创新与价值 | ● | — |
| 问题与改进意识 | ● | — |
---
## 7. 常见问题
- **账号密码能改吗?** 不能。修改后评审系统无法拉取仓库,影响评审结果。
- **仓库是自己建吗?** 不用。组委会已统一创建,直接 push 即可。
- **中期成果要全部吗?** 不用,任选已完成部分提交即可;最终提交赛道二必须全部。
- **视频时长?** 发表会视频 ≤10 分钟;仓库内演示视频赛道一 ≤15 分钟、赛道二 ≤5 分钟。
- **如何判断自己是否提交成功?** push 后验证服务器可拉取;8/31 后 1 周内组委会反馈采集情况。
- **中期评价怎么算?** 中期仅 AI 评审(无人工),占最终成绩 20%;最终评价(AI + 人工)占 80%,AI 与人工总分一致。
---
## 8. 联系方式
- 提交异常 / 采集异常:联系组委会(联系方式另行通知)。
- 技术栈 / AI API 问题:参考提交规范 §9 所需资源。
---
*本文档与《评审规则详细说明》《评审表》配套使用。*
+134
View File
@@ -0,0 +1,134 @@
# 逸飞冲天 评审问题分析
> 版本:2026-08-31
> 对象:赛道一 · 逸飞冲天(T1-SD0401COBOL→Java/Spark 迁移验证平台)
> 目的:记录评审中发现的作品缺陷,说明评分差距的成因,供后续修复参考
---
## 1. 结论速览
**逸飞冲天是"工作量巨大但未完成交付"的典型**:代码量、测试用例、基准程序都是全赛最多,但**核心交付物无法真实运行**——测试一个都跑不起来、Web 服务无法启动。这导致其运行验证类维度(实现完整度、效果与数据)被正确封顶,总分明显低于真正可运行的作品。
| 项 | 数值 |
|:---|:---:|
| 评审总分 | **126 / 200** |
| A 阶段(静态) | 105 / 140 |
| B 阶段(运行验证) | 21 / 60 |
| 前端验证 | ❌ 服务无法启动 |
| 测试验证 | ❌ 830 用例 7 处导入错误,无法运行 |
---
## 2. 发现的问题
### 问题 1:测试用例无法运行(最严重)
**现象**`python -m pytest` 收集 830 个测试用例时 **7 处导入错误**,测试完全无法运行。
**证据**
```
ERROR tests/test_golden.py
ERROR tests/test_orchestrator.py
ERROR tests/test_preprocessor.py
!!! Interrupted: 7 errors during collection !!!
830 tests collected, 7 errors in 5.57s
```
**根因**:测试文件用**扁平模块导入**,但对应模块不存在或位置不对。例如 `tests/test_golden.py:15`
```python
from preprocessor import CopybookPreprocessor # ← 根目录没有 preprocessor.py
```
实际解析逻辑在 `cobol_testgen/read.py` 等子包内,测试引用的模块路径与真实结构不匹配。
**影响**
- 声称的 830 个测试用例、覆盖率验证全部**无法执行**
- "效果与数据"维度(测试通过、覆盖率)只能给最低分
---
### 问题 2:Web 服务无法启动(前端不可访问)
**现象**`python -m uvicorn web.api:app` 启动即报错,前端页面(upload.html / result.html)无法访问。
**证据**
```
ImportError: cannot import name 'check_coverage' from 'cobol_testgen'
```
**根因**`orchestrator.py:12` 导入了不存在的函数:
```python
from cobol_testgen import extract_structure, generate_data, incremental_supplement, check_coverage
```
`cobol_testgen/__init__.py` 中**没有导出 `check_coverage`**(实际有 `mark_coverage` 等,函数名不一致)。
**影响**
- Web 前端(上传页面、结果页面)**实际无法访问**
- "实现完整度与稳定性"因前端不可访问被封顶
---
### 问题 3:Python 包安装结构不完整
**现象**`pip install -e .` 无法正确安装为可导入的包。
**证据**`pyproject.toml``[project]` 声明(name、dependencies),但**缺少 `[tool.setuptools]` 的 packages 配置**,且项目依赖 `from preprocessor import``from orchestrator import` 这类**根级扁平导入**——只有把项目根目录加入 `sys.path` 才能工作,标准安装包结构下无法运行。
**根因**:项目组织为根级模块(main.py、orchestrator.py、config.py)而非规范包(src/ 布局),测试与 Web 层依赖 `sys.path.insert` 临时路径 hack,导致:
- `pip install -e .` 装完仍无法导入(问题 1 的放大)
- 缺少 setup.py/setup.cfg,打包配置不完整
---
### 问题 4:声称与实现不一致(文档一致性低)
**现象**:README 声称大量能力,但部分无法验证或与实现不符。
**证据**
- 声称"非阻塞路径枚举 O(N) 算法"、"MC/DC 条件覆盖"、"DB 种子键一致性"等,但测试跑不起来,无法验证
- 之前评审中"文档声称-代码实现一致性"仅 36%,核心功能声称与实际存在出入
**影响**:场景价值、演示与文档维度被降权(真实性绑定)。
---
## 3. 与可运行作品(零号)的对比
| 维度 | 零号 (158) | 逸飞冲天 (126) | 差距原因 |
|:---|:---:|:---:|:---|
| 文件数 | 604 | 815 | 逸飞冲天更多 |
| 测试用例 | 619 | 830 | 逸飞冲天更多 |
| **测试可运行** | ✅ 依赖装好后 619 全过 | ❌ 830 用例 7 导入错误 | **逸飞冲天测试结构缺陷** |
| **Web 可访问** | ✅ 服务真实启动可访问 | ❌ 服务启动即报错 | **逸飞冲天导入错误** |
| **实现完整度** | 28/35 | 17/35 | 前端封顶 |
| **效果与数据** | 21/25 | 4/25 | 测试无法运行 |
**核心差异**:零号是"能跑起来的完整作品",逸飞冲天是"写了大量代码但关键链路(测试、Web)跑不起来"。
---
## 4. 评分合理性说明
新评审规则下,逸飞冲天 126 分是**合理**的:
- **静态维度给分**(架构 11、Agent核心 22、规模 23):认可其**工作量和设计投入**
- **运行验证维度封顶**(实现完整度 17、效果数据 4):反映其**关键交付物无法真实运行**
这正是"代码量大 ≠ 完成度高"的区分——**工作量值得认可,但未达到可交付标准**。
---
## 5. 建议修复方向(供参赛队伍参考)
1. **修正测试导入**:将 `tests/` 下的扁平导入(`from preprocessor import``from orchestrator import`)改为正确的包路径(如 `from cobol_testgen.read import ...`),或在 pytest 配置 `pythonpath` 指向正确模块
2. **修正 web 导入**`orchestrator.py``check_coverage` 改为实际存在的函数名(`mark_coverage` 或实现该函数)
3. **完善包结构**:在 `pyproject.toml` 增加 `[tool.setuptools]` packages 配置,或改用 src/ 布局,确保 `pip install -e .` 后可导入
4. **修复后重评**:修正上述问题后,测试应能运行、Web 应能启动,运行验证维度分数将恢复正常
---
*本文档基于 2026-08-31 评审与实测生成,证据可复现(clone 最新仓库后执行 pytest / uvicorn 可验证)。*
-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);
+142 -15
View File
@@ -23,14 +23,22 @@ async function downloadPdf(url: string, options?: { silent?: boolean }) {
filename = plain ? plain[1] : '';
}
if (!filename) filename = url.includes('summary') ? '汇总报告.pdf' : url.includes('deliverables') ? '成果物清单.csv' : '报告.pdf';
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
if (!options?.silent) alert(`已开始下载:${filename}(保存到浏览器下载目录)`);
// 2026-08-28 修复:不得在 click 后立即 alertalert 阻塞主线程,且过早 revokeObjectURL
// 会在浏览器真正落盘前销毁 blob,导致"提示成功但下载目录无文件")。延迟 revoke 到足够久,
// alert 改由 setTimeout 延后,不阻断下载事件循环。
const revoke = () => URL.revokeObjectURL(blobUrl);
setTimeout(revoke, 60000);
window.addEventListener('unload', revoke);
if (!options?.silent) {
setTimeout(() => alert(`已开始下载:${filename}(保存到浏览器下载目录)`), 300);
}
return true;
} catch (e: any) {
alert('下载失败: ' + (e?.message || e));
@@ -200,6 +208,7 @@ function EntryManager({ projectId, track }: { projectId: string; track?: string
const [search, setSearch] = useState('');
const [selected, setSelected] = useState<Set<string>>(new Set());
const [detail, setDetail] = useState<any>(null);
const [serviceInputs, setServiceInputs] = useState<Record<string, string>>({});
const [showImport, setShowImport] = useState(false);
const [csvText, setCsvText] = useState('');
const [importResult, setImportResult] = useState<any>(null);
@@ -255,14 +264,25 @@ function EntryManager({ projectId, track }: { projectId: string; track?: string
} catch (err: any) { alert(err.message || '操作失败'); }
};
// §2.2 人工构建确认:a_done 提供「确认构建完成 / 构建失败」按钮,结果传给 /verify
const doVerify = async (entryId: string, buildStatus: 'done' | 'failed') => {
// §2.2 人工构建确认:a_done 提供「确认构建完成 / 构建成功但无前端 / 构建失败」按钮,结果传给 /verify
const doVerify = async (entryId: string, buildStatus: 'done' | 'failed' | 'done_no_ui') => {
try {
await api.request('POST', `/projects/${projectId}/entries/${entryId}/verify`, { build_status: buildStatus });
await load();
} catch (err: any) { alert(err.message || '启动系统验证失败'); }
};
// §2.5.1 浏览器验证恢复(awaiting_browse):评委手动启动服务后填地址,调 /verify-browse 恢复
const doVerifyBrowse = async (entryId: string) => {
const url = (serviceInputs[entryId] || '').trim();
if (!url) { alert('请先填写手动启动后的服务访问地址'); return; }
try {
await api.request('POST', `/projects/${projectId}/entries/${entryId}/verify-browse`, { service_url: url });
setServiceInputs({ ...serviceInputs, [entryId]: '' });
await load();
} catch (err: any) { alert(err.message || '恢复浏览器验证失败'); }
};
const doDelete = async (entryId: string, title: string) => {
if (!confirm(`删除条目"${title}"`)) return;
try { await api.request('DELETE', `/projects/${projectId}/entries/${entryId}`); await load(); } catch (err: any) { alert(err.message || '删除失败'); }
@@ -360,13 +380,13 @@ function EntryManager({ projectId, track }: { projectId: string; track?: string
const statusBadge = (s: string) => {
const map: Record<string, string> = {
pending: 'badge-gray', queued: 'badge-amber', cloning: 'badge-blue', analyzing: 'badge-blue',
a_done: 'badge-purple', verifying: 'badge-blue',
a_done: 'badge-purple', verifying: 'badge-blue', awaiting_browse: 'badge-amber',
review_done: 'badge-green', admin_reviewed: 'badge-green',
clone_fail: 'badge-red', analysis_fail: 'badge-red', failed: 'badge-red', cancelled: 'badge-gray',
};
const label: Record<string, string> = {
pending: '待评审', queued: '排队中', cloning: '克隆中', analyzing: '分析中',
a_done: 'A阶段完成', verifying: '系统验证中',
a_done: 'A阶段完成', verifying: '系统验证中', awaiting_browse: '等待浏览器验证',
review_done: '已完成', admin_reviewed: '已修正',
clone_fail: '克隆失败', analysis_fail: '分析失败', failed: '失败', cancelled: '已取消',
};
@@ -505,9 +525,22 @@ function EntryManager({ projectId, track }: { projectId: string; track?: string
{['queued', 'cloning', 'analyzing'].includes(e.status) && <button className="btn-action warn" onClick={() => doAction('cancel', e.id)}></button>}
{e.status === 'a_done' && <button className="btn-action" onClick={() => openEdit(e)}></button>}
{e.status === 'a_done' && <button className="btn-action" style={{ borderColor: '#10b981', color: '#10b981' }} onClick={() => doVerify(e.id, 'done')}></button>}
{e.status === 'a_done' && <button className="btn-action" style={{ borderColor: '#f59e0b', color: '#f59e0b' }} title="构建成功但确认无前端界面(触发实现完整度封顶)" onClick={() => doVerify(e.id, 'done_no_ui')}></button>}
{e.status === 'a_done' && <button className="btn-action" style={{ borderColor: '#ef4444', color: '#ef4444' }} onClick={() => doVerify(e.id, 'failed')}></button>}
{e.status === 'a_done' && <button className="btn-action" onClick={() => openDetail(e.id)}></button>}
{e.status === 'verifying' && <button className="btn-action warn" onClick={() => doAction('cancel', e.id)}></button>}
{e.status === 'awaiting_browse' && (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<input
value={serviceInputs[e.id] ?? ''}
onChange={ev => setServiceInputs({ ...serviceInputs, [e.id]: ev.target.value })}
placeholder="服务地址,如 http://127.0.0.1:8000"
className="input-field"
style={{ width: 200, padding: '6px 10px' }}
/>
<button className="btn-action" style={{ borderColor: '#10b981', color: '#10b981' }} onClick={() => doVerifyBrowse(e.id)}></button>
</span>
)}
{['clone_fail', 'analysis_fail', 'failed'].includes(e.status) && <button className="btn-action" onClick={() => doAction('retry', e.id)}></button>}
{['review_done', 'admin_reviewed'].includes(e.status) && <button className="btn-action" onClick={() => openDetail(e.id)}></button>}
{['review_done', 'admin_reviewed'].includes(e.status) && <button className="btn-action" onClick={() => doAction('start', e.id)}>×3</button>}
@@ -544,6 +577,7 @@ function EntryManager({ projectId, track }: { projectId: string; track?: string
<select value={editEntry.build_status || ''} onChange={e => setEditEntry({ ...editEntry, build_status: e.target.value })} className="select-field">
<option value=""></option>
<option value="done"></option>
<option value="done_no_ui"></option>
<option value="failed"></option>
</select>
<div className="flex gap-8">
@@ -691,7 +725,7 @@ function DetailPanel({ entry, projectId, onClose, onSave }: { entry: any; projec
{entry.attempt > 1 && <div className="retake-badge"> {entry.attempt} </div>}
</div>
{entry.plagiarism_json && (() => {
{entry.category_tag === 'L2考核' && entry.plagiarism_json && (() => {
let p: any = null;
try { p = JSON.parse(entry.plagiarism_json); } catch { }
if (!p || !p.flags || p.flags.length === 0) return null;
@@ -817,6 +851,21 @@ function DetailPanel({ entry, projectId, onClose, onSave }: { entry: any; projec
<span className={`dim-comment-btn ${d.comment ? '' : 'is-empty'}`} onClick={() => setEditingComment(idx)} title="点击编辑评语">{d.comment || '点击填写评语'}</span>
)}
{d.verifiability?.note && <div style={{ fontSize: 11, color: 'var(--text-secondary)', opacity: 0.8, marginTop: 4 }}>{d.verifiability.note}</div>}
{Array.isArray(d.checks) && d.checks.length > 0 && (
<div style={{ marginTop: 6, fontSize: 11, borderTop: '1px dashed var(--border)', paddingTop: 4 }}>
{d.checks.map((c: any, ci: number) => (
<div key={ci} style={{ display: 'flex', alignItems: 'flex-start', gap: 6, marginTop: 3 }}>
<span className={`badge ${c.depth === 'real' ? 'badge-green' : c.depth === 'partial' ? 'badge-amber' : 'badge-red'}`} style={{ flexShrink: 0 }}>
{c.depth === 'real' ? '真实' : c.depth === 'partial' ? '部分' : '占位'}
</span>
<span style={{ color: 'var(--text-secondary)' }}>
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{c.feature}</span>
{c.entry ? <span style={{ opacity: 0.7 }}> · {c.entry}</span> : null}
</span>
</div>
))}
</div>
)}
</td>
<td><div className="suggestion-cell">{d.suggestion || '-'}</div></td>
</tr>
@@ -855,6 +904,21 @@ function DetailPanel({ entry, projectId, onClose, onSave }: { entry: any; projec
<span className={`dim-comment-btn ${d.comment ? '' : 'is-empty'}`} onClick={() => setEditingComment(idx)} title="点击编辑评语">{d.comment || '点击填写评语'}</span>
)}
{d.verifiability?.note && <div style={{ fontSize: 11, color: 'var(--text-secondary)', opacity: 0.8, marginTop: 4 }}>{d.verifiability.note}</div>}
{Array.isArray(d.checks) && d.checks.length > 0 && (
<div style={{ marginTop: 6, fontSize: 11, borderTop: '1px dashed var(--border)', paddingTop: 4 }}>
{d.checks.map((c: any, ci: number) => (
<div key={ci} style={{ display: 'flex', alignItems: 'flex-start', gap: 6, marginTop: 3 }}>
<span className={`badge ${c.depth === 'real' ? 'badge-green' : c.depth === 'partial' ? 'badge-amber' : 'badge-red'}`} style={{ flexShrink: 0 }}>
{c.depth === 'real' ? '真实' : c.depth === 'partial' ? '部分' : '占位'}
</span>
<span style={{ color: 'var(--text-secondary)' }}>
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{c.feature}</span>
{c.entry ? <span style={{ opacity: 0.7 }}> · {c.entry}</span> : null}
</span>
</div>
))}
</div>
)}
</td>
<td><div className="suggestion-cell">{d.suggestion || '-'}</div></td>
</tr>
@@ -866,14 +930,77 @@ function DetailPanel({ entry, projectId, onClose, onSave }: { entry: any; projec
</div>
)}
{entry.snapshots?.length > 0 && (
<details className="history-section">
<summary>{entry.snapshots.length} </summary>
{entry.snapshots.map((s: any) => (
<div key={s.id} className="history-item">
{s.attempt} · {new Date(s.created_at).toLocaleString()}
{entry.feature_inventory && (() => {
let fi: any = null;
try { fi = JSON.parse(entry.feature_inventory); } catch { }
if (!Array.isArray(fi) || fi.length === 0) return null;
const real = fi.filter((f: any) => f.depth === 'real').length;
const partial = fi.filter((f: any) => f.depth === 'partial').length;
const stub = fi.filter((f: any) => f.depth === 'stub').length;
const MAX_SHOW = 100;
return (
<details className="history-section" style={{ marginBottom: 16 }}>
<summary style={{ cursor: 'pointer' }}>
{fi.length} {real} · {partial} · {stub}
</summary>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 8, marginLeft: 4 }}>
<div style={{ opacity: 0.7, marginBottom: 8 }}>Map-Reduce README/线</div>
{fi.slice(0, MAX_SHOW).map((f: any, i: number) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
<span className={`badge ${f.depth === 'real' ? 'badge-green' : f.depth === 'partial' ? 'badge-amber' : 'badge-red'}`} style={{ flexShrink: 0 }}>
{f.depth === 'real' ? '真实' : f.depth === 'partial' ? '部分' : '占位'}
</span>
<span>{f.feature}</span>
{Array.isArray(f.files) && f.files.length > 0 && <span style={{ opacity: 0.7 }}> · {f.files.slice(0, 2).join(', ')}</span>}
</div>
))}
{fi.length > MAX_SHOW && <div style={{ opacity: 0.7, marginTop: 4 }}> {fi.length} {MAX_SHOW} </div>}
</div>
))}
</details>
);
})()}
{entry.snapshots?.length > 0 && (
<details className="history-section" open>
<summary>{entry.snapshots.length} {entry.aggregate_score != null && ` · 最终 ${entry.aggregate_score}${entry.is_formal ? `(聚合 ${entry.aggregate_count} 次中位数)` : `(初评 ${entry.aggregate_count} 次)`}`}</summary>
{entry.snapshots.map((s: any) => {
let aScore: number | null = null, bScore: number | null = null;
try {
// ai_report.dimensions 无 stage 字段;用 standard_snapshot(entry.dimensions) 的 stage 映射维度名
const stageMap: Record<string, string> = {};
for (const sd of (entry.dimensions || [])) stageMap[sd.name] = String(sd.stage || '').toUpperCase();
const rep = JSON.parse(s.ai_report);
if (Array.isArray(rep.dimensions)) {
for (const d of rep.dimensions) {
const st = stageMap[d.name] || String(d.stage || '').toUpperCase();
if (st === 'A') aScore = (aScore ?? 0) + (Number(d.score) || 0);
else if (st === 'B') bScore = (bScore ?? 0) + (Number(d.score) || 0);
}
}
} catch {}
const total = s.score ?? (aScore ?? 0) + (bScore ?? 0);
return (
<div key={s.id} className="history-item" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<div>
<strong> {s.attempt} · {total} </strong>
<span style={{ marginLeft: 8, fontSize: 12, color: '#666' }}>
{aScore != null ? `A:${aScore}` : ''}{aScore != null && bScore != null ? ' + ' : ''}{bScore != null ? `B:${bScore}` : ''}
{aScore == null && bScore == null ? '(无维度明细)' : ''}
</span>
</div>
<span style={{ fontSize: 12, color: '#999' }}>{new Date(s.created_at).toLocaleString()}</span>
</div>
);
})}
{entry.snapshots.length > 0 && entry.aggregate_score != null && (
<div className="history-item" style={{ marginTop: 6, paddingTop: 6, borderTop: '1px dashed #ddd' }}>
<strong>{entry.aggregate_score} </strong>
<span style={{ marginLeft: 8, fontSize: 12, color: '#666' }}>
{entry.snapshots.map((s: any) => s.score ?? (s.attempt ?? '?')).join(' · ')}
{entry.is_formal ? '(正式分)' : '(未达聚合样本数)'}
</span>
</div>
)}
</details>
)}