diff --git a/AGENTS.md b/AGENTS.md index f6d75d6..108d8a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,7 @@ Phase 3: 确定性校准(computeCalibration) + 硬规则引擎 |------|------| | `review.service.ts` (~1500行) | 评审引擎:clone→tryBuild→tryBrowse→AI→校准(computeCalibration)→硬规则→出分 | | `standards.ts` | `parseDimensions` 解析标准 MD 为维度列表 | +| `l2-topics.ts` | L2考核选题元数据(11命题题+自选题,`config/l2-topics.json`):难度赋分 cap、功能完整性拆分、验收要点注入 | | `entries.ts` | 条目 CRUD + 触发评审 + PDF 导出 | | `projects.ts` | 项目 CRUD + 汇总排名 | | `db.ts` | SQLite 初始化 + migrations(ALTER TABLE try/catch) | @@ -161,18 +162,19 @@ cloneRepo → discoverFiles → countCodeStats → tryBuild → tryBrowse/trySta - 正则在 `standards.ts`,修复后支持空内容(`content: ""`) - 从 `## 维度名(分数)` 解析,必须紧跟 `\n## ` 或字符串结束 - 过滤分区/说明性标题:`评审维度`、`合格判定`、`成果物清单`、`第X部分:` 等不进入维度列表 -- group 推断:`## [Q2] xxx`(显式)或 `## 2-A. xxx` / `## 2-A:xxx`(人才测评追加维度前缀)→ group=Q2;其余 common -- 验证:三个标准模板解析结果应固定为——赛道一 12维/150、赛道二 8维/100、人才测评 common100+maxBonus50/effective150(Q2-Q6 分组正确) +- group 推断:`## [Q2] xxx`(显式)或 `## 2-A. xxx` / `## 2-A:xxx`(题目追加维度前缀)→ group=Q2;其余 common +- 验证:标准模板解析结果应固定为——赛道一 12维/150、赛道二 8维/100、L2考核 7维/100(全 common);人才测评已整合进 L2考核(2026-08-23),不再单独验证 ### entries 表 - `UNIQUE(project_id, repo_url)` 约束 - `service_url TEXT DEFAULT ''`(migration 添加) -- `build_status TEXT DEFAULT ''`(2026-08-18,migration 添加):赛道二/人才测评单阶段人工构建确认,''=未确认(自动构建) / done / failed;赛道一 B 阶段不存此列,走 /verify 请求体 +- `build_status TEXT DEFAULT ''`(2026-08-18,migration 添加):赛道二/L2考核单阶段人工构建确认,''=未确认(自动构建) / done / failed;赛道一 B 阶段不存此列,走 /verify 请求体 - `standard_snapshot` 存标准快照(评审时标准被修改也不影响已评审结果) - `review_snapshots` 存每次评审历史 +- L2考核专属字段:`selected_topic`(选题 01~11/self,决定 max_score_cap 与难度赋分)、`self_registration`(自选题登记 JSON:登记编号+功能清单快照) - force-review(测试端点)需 `ADMIN_TEST_TOKEN=true` **且** `NODE_ENV=test` 双条件才开启;生产 `node dist/index.js` 即使 flag 泄漏也打不开 -- **赛道必选(2026-08-13)**:创建/编辑项目必须传 track(赛道一/赛道二/人才测评),缺省返回 400 `赛道为必选项`;前端下拉首项"请选择赛道(必选)",create() 校验 `if (!track) setError('请选择赛道')`。注意:有赛道会自动导入对应默认标准(`projects.ts` POST 里 `loadDefaultStandard`)——创建项目的测试用例必须带合法 track,否则建出的项目无赛道且后续标准解析行为不同。**e2e 陷阱**:依赖"自定义标准快照"的用例(如 `hardcode-fixes.spec.ts` 3.1/3.2)在带 track 创建项目后,自动标准会抢占 `resolveStandard` 匹配,导致条目快照不是测试上传的标准——需先调用 `apiClearStandards(request, token, pid)` 清掉自动标准再上传自定义标准 +- **赛道必选(2026-08-13)**:创建/编辑项目必须传 track(赛道一/赛道二/L2考核),缺省返回 400 `赛道为必选项`;前端下拉首项"请选择赛道(必选)",create() 校验 `if (!track) setError('请选择赛道')`。注意:有赛道会自动导入对应默认标准(`projects.ts` POST 里 `loadDefaultStandard`)——创建项目的测试用例必须带合法 track,否则建出的项目无赛道且后续标准解析行为不同。**e2e 陷阱**:依赖"自定义标准快照"的用例(如 `hardcode-fixes.spec.ts` 3.1/3.2)在带 track 创建项目后,自动标准会抢占 `resolveStandard` 匹配,导致条目快照不是测试上传的标准——需先调用 `apiClearStandards(request, token, pid)` 清掉自动标准再上传自定义标准 ### 本地评审样本 @@ -254,4 +256,6 @@ cd server && node -e "const D=require('better-sqlite3');const d=new D('data/ai-r **赛道二(IDE+开发范式创新赛):** 场景价值、开发范式、工具使用、实现完整度、规模·功能点、演示与文档、AI使用日志、效果评估与数据(总分100) -当前维度 guideline 只覆盖标准维度名(如"场景价值与合理性"),赛道一变体名(如"开发范式与架构设计")需扩展匹配关键词。 +**L2考核(AI人才育成认证,7维/100):** 功能完整性30、设计文档10、测试用例与测试结果10、AI协作过程记录15、技术选型与范式运用15、代码质量+README 10、业务场景理解与需求分析10。选题难度赋分:★★+0/★★★+5/★★★★+10,封顶选题 cap(100/105/110)。合格线≥60 且 功能完整性≥15(地板线)。 + +当前维度 guideline 只覆盖标准维度名(如"场景价值与合理性"),赛道一变体名(如"开发范式与架构设计")需扩展匹配关键词。L2考核 7 维标准自带分档锚点与交叉验证规则,content 优先不触发内置兜底。 diff --git a/docs/L2考核成果物提交规范.md b/docs/L2考核成果物提交规范.md index 9d0a796..3c7e9e8 100644 --- a/docs/L2考核成果物提交规范.md +++ b/docs/L2考核成果物提交规范.md @@ -10,7 +10,7 @@ > > **提交截止:2026年9月底(9月30日)**。自题目发布日起至 9月30日,将成果物 `git push` 到 Gitea 仓库 main 分支(以仓库最后推送时间为准)。 > -> 超过截止时间未提交,按以下迟交规则处理:迟交 1~3 个工作日扣 5 分,4~7 个工作日扣 10 分,超过 7 个工作日本题按 0 分处理并需重做(重做细则见 §12.3)。 +> 超过截止时间未提交,按以下迟交规则处理:迟交 1~3 个工作日扣 5 分,4~7 个工作日扣 10 分,超过 7 个工作日**按 0 分处理、不再评审**。 > > 请务必在截止前完成:账号登录 → 成果物放入仓库 → `git push` 到 main 分支,并确认服务器上能看到最新提交。 @@ -28,7 +28,7 @@ - §10 独立完成与查重红线 | §11 源码可运行前提 **第三部分 · 考核标准与纪律** -- §12 考核标准(7维评审要点 / 合格线与赋分 / 重做细则) +- §12 考核标准(7维评审要点 / 合格线与赋分 / 结果申诉) - §13 提交前自查清单 | §14 违规后果 **第四部分 · 命题题详细需求(选定题目后细读对应章节)** @@ -66,7 +66,7 @@ L2(Level 2)是 AI 人才育成体系中的能力等级,定义为:**能 | 开发工具 | 推荐 OpenCode / Claude Code / GitHub Copilot(不作强制);LLM API 自备 | | 评审流程 | 系统自动拉取仓库 → 成果物齐全性自动检测 + 跨仓库查重初筛 → 评审者按 7 维框架人工评分 → 结果反馈 | | 合格标准 | 合格线 **≥60 分**(命题题含难度赋分,满分最高110) | -| 未达标处理 | 按重做细则重做一次(5个工作日内),重做评分上限 80 分;细则见 §12.3 | +| 未达标处理 | 评审结论为不合格(总分<60 或 功能完整性<15 地板线);结果申诉见 §12.4 | ### 1.3 考核目标:验证什么 @@ -346,7 +346,7 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** **处理规则**: - 初筛告警由评审者人工复核认定;单次全量 push 等行为信号本身**不构成违规认定**,仅作为复核参考——被标记者应能在 AGENTS.md 与 git 历史中给出独立完成的依据 -- **查实抄袭:本题按 0 分处理并需重做(重做上限 80 分),处理结果记入考核档案** +- **查实抄袭:本题按 0 分处理,处理结果记入考核档案** - 允许查阅公开资料与使用 AI 生成,但须如实记录(见 §8);未记录的他人代码片段视为抄袭 - 组委会保留对可疑情形**约谈核实**的权利:可要求受验者现场讲解任意一段核心实现的思路与取舍;拒绝配合或讲解与提交代码明显不符的,按查实抄袭处理 @@ -386,14 +386,7 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** - **功能完整性评分锚点**:命题题按该题验收基准表逐项评定;自选题按 README 声明的功能清单逐项核对实现情况评定(结合「声称 vs 实测」验证,未实现或与声明不符的功能不计分) - **功能完整性地板线**:该维度得分低于 15 分(不足满分一半)时,无论总分多少均判为**不合格**——防止「文档精美、功能空壳」的提交过线 -### 12.3 重做细则 - -- 未达合格线:自通知之日起 **5 个工作日内**重做,重做评分最高记 **80 分**,每题限重做 **1 次**;再次未达线则总评为不合格 -- 重做允许**更换命题**(在 01~11 中另行选择,自选题亦可修订原方案),按新选题的验收基准评分,难度赋分随新选题调整 -- 重做提交同样适用迟交规则:逾期扣分,超过 7 个工作日视为**放弃重做资格**,总评不合格 -- 查实抄袭者不适用正常重做流程,按 §14 违规后果处理 - -### 12.4 结果申诉 +### 12.3 结果申诉 - 对评分结果或查重认定有异议的,可在收到结果之日起 **3 个工作日内**向组委会提交一次复核申请(列明异议点与依据) - 由**未参与原评审**的人员进行复核,**5 个工作日内**书面答复;复核结论为最终结论 @@ -424,13 +417,13 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** | 情形 | 后果 | |------|------| -| 超过截止时间未提交/未push | 按迟交规则扣分;超7个工作日按0分并需重做 | +| 超过截止时间未提交/未push | 迟交 1~3 个工作日扣 5 分,4~7 个工作日扣 10 分;超 7 个工作日按 0 分处理、不再评审 | | 仓库名、分支、凭据不符合规范 | 评审系统无法拉取,按未提交处理 | | 必填成果物缺失或命名不符 | 对应成果物判定为未提交,相关维度扣分 | | README 声称功能与实测不符 | 真实性扣分 | | AI 日志缺失或空白 | 相关维度扣分 | | 源码无法按 README 运行 | 后续所有评分项扣分 | -| 查实抄袭/代做 | 本题 0 分 + 重做(上限80分),记入考核档案 | +| 查实抄袭/代做 | 本题 0 分,记入考核档案 | | 使用真实业务/客户数据 | 视情节按信息安全违规处理,相关维度扣分至取消资格 | --- @@ -1152,9 +1145,11 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** | # | 要求 | 说明 | |---|------|------| | 1 | **证据型报告** | 每个检出问题必须定位到具体位置(`文件名:行号` 或 `章节编号/条目原文摘录`),附问题描述;不允许只输出整体评价而无问题定位 | -| 2 | **规则外置** | 检查规则必须写在独立配置文件中(YAML/JSON/Markdown均可),新增或修改一条规则不需要改动程序代码;演示时须现场新增1条规则并验证生效 | +| 2 | **规则外置** | 检查规则必须写在独立配置文件中(YAML/JSON/Markdown均可),新增或修改一条规则不需要改动程序代码;须提交自动化验证脚本与运行日志(放 tests/),证明「新增一条规则后不改变代码即可生效」 | | 3 | **分级判定** | 问题分为 Critical / Major / Minor 三级;报告包含各级数量统计和明确的通过结论(通过 / 有条件通过 / 不通过) | -| 4 | **企业标准外置** | 各阶段适用的企业内部标准(文档格式规范 / 编码规约 / 测试基准等)以独立配置文件提供,工具按配置执行检查;标准的具体条目由项目方定义,工具不得硬编码。样本数据须附「标准配置A/B两套」,演示切换B套后检出结果随之变化 | +| 4 | **企业标准外置** | 各阶段适用的企业内部标准(文档格式规范 / 编码规约 / 测试基准等)以独立配置文件提供,工具按配置执行检查;标准的具体条目由项目方定义,工具不得硬编码。样本数据须附「标准配置A/B两套」,评审者切换B套后检出结果随之变化 | + +> **组内共性提交物补充**:第 2、4 条的「规则/标准外置生效」验证,以自动化验证脚本 + 运行日志形式提交(放 `tests/`),脚本演示「新增/修改一条规则(或切换 A/B 标准)后不改变代码即可生效、检出结果随之变化」。评审系统按脚本可复现执行核验,不做现场演示。 --- @@ -1225,7 +1220,7 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** | 可测试性评估 | 正确标识植入的不可测试需求 | 附具体可操作的改写建议 | 对照缺陷标注清单核对 | | 编写规范检查 | 配置的格式违规能正确检出 | 支持标准A/B切换,检出结果随之变化 | 对照缺陷标注清单核对+切换标准演示 | | 报告规范 | 分级+章节定位完整 | 定位含条目原文摘录 | 抽查报告中定位是否准确 | -| 规则外置生效 | 规则在独立配置文件中维护 | 现场新增1条规则不改代码生效 | 演示验证 | +| 规则外置生效 | 规则在独立配置文件中维护 | 新增1条规则不改代码生效,有自动化脚本+运行日志佐证 | 运行提交的验证脚本核验 | #### 提交物清单 @@ -1321,8 +1316,8 @@ README 是评审者了解作品的第一个入口,**必须尽可能说清楚** | 追溯矩阵映射 | 10条需求正确建立≥8条映射 | 全部建立且无误报 | 对照缺陷标注清单核对 | | 未覆盖与孤儿标记 | 两类问题均能正确标出 | 无误报 | 对照缺陷标注清单核对 | | 接口一致性 | 植入2处矛盾至少检出1处 | 全部检出并引用两处定义原文 | 对照缺陷标注清单核对 | -| 矩阵可视化 | 表格形式展示完整映射关系 | 支持点击跳转到对应位置 | 操作演示验证 | -| 规则外置生效 | 必备章节清单在配置文件中维护 | 现场调整清单后重新评审生效 | 演示验证 | +| 矩阵可视化 | 表格形式展示完整映射关系 | 支持点击跳转到对应位置 | 运行提交的脚本/截图验证 | +| 规则外置生效 | 必备章节清单在配置文件中维护 | 调整清单后重新评审生效,有自动化脚本+运行日志佐证 | 运行提交的验证脚本核验 | #### 提交物清单 @@ -1416,7 +1411,7 @@ AI生成代码大量合入代码库后,出现特有的高频缺陷模式: | 空值防护 | 典型裸访问模式检出率≥70% | 全部检出且无误报 | 对照缺陷标注清单核对 | | 编码规约检查 | 配置的规约违规(命名/注释)能正确检出 | 支持规约A/B切换,检出结果随之变化 | 对照缺陷标注清单核对+切换规约演示 | | 扫描范围控制 | 配置的排除目录不被扫描 | 白名单机制生效 | 故意配置后运行验证 | -| 报告规范与规则外置 | 分级+file:line定位完整 | 修复建议含示例代码;现场新增规则生效 | 抽查报告+演示验证 | +| 报告规范与规则外置 | 分级+file:line定位完整 | 修复建议含示例代码;新增规则不改代码生效,有脚本佐证 | 抽查报告+运行验证脚本 | #### 提交物清单 @@ -1513,7 +1508,7 @@ AI辅助生成测试用例后,测试数量激增但质量参差: | 无断言测试识别 | 无断言测试100%标记 | 附最小断言补充建议 | 对照缺陷标注清单核对 | | 基准符合性检查 | 配置的基准违规(缺预期结果等)能正确检出 | 支持基准A/B切换,检出结果随之变化 | 对照缺陷标注清单核对+切换基准演示 | | 用例评级 | 每个测试用例有A/B/C评级 | C级用例附针对性改进建议 | 抽查评级合理性 | -| 报告规范与规则外置 | 评级结果+覆盖矩阵完整展示 | 评级标准在配置文件中可调整 | 演示验证 | +| 报告规范与规则外置 | 评级结果+覆盖矩阵完整展示 | 评级标准在配置文件中可调整,有脚本佐证 | 运行验证脚本核验 | #### 提交物清单 diff --git a/server/config/l2-topics.json b/server/config/l2-topics.json new file mode 100644 index 0000000..e017bf3 --- /dev/null +++ b/server/config/l2-topics.json @@ -0,0 +1,210 @@ +{ + "version": "2026-08-23", + "note": "L2考核选题元数据。maxScoreCap=共通100分+难度赋分;funcBreakdown 为功能完整性30分的题目专属拆分,注入该维度子Agent prompt;acceptance 为验收基准要点;sampleData 为题目专属样本数据要求。", + "selfTopic": { + "id": "self", + "title": "自选题", + "difficulty": 0, + "maxScoreCap": 100, + "minTests": 3, + "funcBreakdown": "按 README 声明且不低于登记范围的功能清单逐项核对:核心闭环(输入处理→核心逻辑→结果呈现)完整性与各模块实现质量综合评定", + "acceptance": [ + "功能模块≥3个(用户可感知的独立功能单元,工具函数与界面装饰不计)", + "整体规模明显超出单脚本范畴(通常数百行及以上)", + "README 功能声明不得低于事前登记范围的核心功能,未经登记更新的缩减按未实现计" + ], + "sampleData": "全部数据须脱敏或虚构,禁止真实业务/客户数据" + }, + "topics": [ + { + "id": "01", + "title": "年度满意度调查数据的自动处理与分析", + "group": "business", + "difficulty": 2, + "maxScoreCap": 100, + "minTests": 3, + "funcBreakdown": "模板管理5分(上传+自动识别+确认)/ 数据导入与统计10分(导入+多维度统计+年度对比)/ 权限区分5分(两种角色隔离)/ 看板展示5分(图表+下钻)/ 智能分析5分(薄弱维度识别+改善建议)", + "acceptance": [ + "模板可变应对:样本模板A正确解析为合格线,列名/列数不同的模板B也能解析为满分", + "统计正确性:小样本手算结果与系统一致,小数位规则统一定义且可对齐", + "年度对比:2年维度名不同仍能正确对齐", + "权限隔离:部门管理员不可查看其他部门明细,实现方式在AGENTS.md说明", + "分析深度:识别得分最低维度为合格线,识别下降趋势+给出可量化改善建议为满分" + ], + "sampleData": "至少2个年度模拟Excel(多Sheet),含正常数据与边界数据(空值/重复/维度名不一致/评分越界等)" + }, + { + "id": "02", + "title": "年度面谈问卷生成与数据分析", + "group": "business", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 3, + "funcBreakdown": "规则配置与低分识别10分 / 面谈问卷生成10分 / 面谈数据导入5分 / 看板展示5分(智能分析分数含在上述各项中不单独计分)", + "acceptance": [ + "低分规则至少2种可配置(绝对阈值/前年比下降/部门特异性,建议3种),支持组合与预览判断理由", + "问卷基于低分维度生成,内容可编辑;按部门/职级定制为满分项", + "话题分类:10条标注样本准确率≥60%合格线、≥80%满分(口径自定须在设计文档写明)", + "共性问题提取:植入5个高频话题检出≥3个合格线", + "改善建议针对具体部门、可量化可执行" + ], + "sampleData": "至少2个年度满意度调查Excel + 至少30条面谈记录(覆盖5个话题分类、3个部门)" + }, + { + "id": "03", + "title": "人事制度RAG检索系统", + "group": "business", + "difficulty": 4, + "maxScoreCap": 110, + "minTests": 5, + "funcBreakdown": "必达25分:文档上传与解析5 / 基础检索10 / 无法回答处理5 / 追问5。加分5分任选其一:检索策略切换 / 检索质量可视化 / Chunk管理 / 制度间关联 / 分析看板(多项实现仍计5分,深度可在其他维度体现)", + "acceptance": [ + "基础检索:5个已知答案问题命中≥3个合格线、5个全中带精确出处为满分", + "无法回答检测:无关问题明确表示无法回答、不编造", + "出处引用:文档名/章节/页码(Word无页码用章节+段落序号)真实可定位", + "追问:至少1次连续追问保持上下文", + "中日文混合检索:同一提问可含中日文混用", + "评分型指标口径自定但须在设计文档写明" + ], + "sampleData": "至少3份模拟制度文档(中/日文各≥1份),多级章节结构,含可关联交叉引用" + }, + { + "id": "04", + "title": "合同智能审查", + "group": "business", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 5, + "funcBreakdown": "合同上传与信息提取5 / 审查等级判断5 / 条款提取与风险标记10 / 审批流程5 / 盖章审批5(签约到期提醒、分析报告分数含在上述各项中)", + "acceptance": [ + "信息提取:名称/签约方/金额/签订日期/合同类型/有效期等字段正确", + "审查等级:按金额阈值(<10万简易/≥10万且<100万标准/≥100万严格-律师模拟邮件)判断正确", + "风险标记:违约金过高(>20%基线)、管辖地不利等不利条款标记,阈值在规则库显式定义可调整", + "审批流程:等级不同审批路径不同,审查完成自动通知发起人(模拟即可)", + "到期管理:距到期≤30天标识待续签、已过期状态正确" + ], + "sampleData": "至少5份模拟合同PDF(含正常与有问题合同、不同金额场景),必须含「距到期日≤30天」与「已过期」样本各≥1份" + }, + { + "id": "05", + "title": "法规信息自动收集爬虫", + "group": "business", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 3, + "funcBreakdown": "数据源管理5 / 信息自动收集10 / 增量采集5 / 结果展示5 / 定期报告5", + "acceptance": [ + "增量采集:同一内容不重复采集,更新后能再次采集", + "内容提取:法规名称/发布机关/发布日期/施行日期/发布类型/摘要/原文链接正确提取", + "关键词筛选:包含词+排除词组合筛选正确,影响度三级可区分", + "错误处理:无效URL记录日志、重试后跳过不影响其他数据源", + "定时调度以配置项或计划任务形式体现即可,验收允许手动触发", + "影响度评估口径自定须在设计文档写明,ground truth 标注清单格式规范随样本提交" + ], + "sampleData": "至少3个不同结构的模拟目标网页HTML" + }, + { + "id": "06", + "title": "外部环境风险信息的自动收集与分析", + "group": "business", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 3, + "funcBreakdown": "信息源管理5 / 自动信息收集5 / 增量采集5 / 智能分类10 / 风险仪表盘5", + "acceptance": [ + "多源采集:至少2种信息源类型(RSS/网页/API)", + "风险分类:5领域分类准确率≥50%合格线、≥80%满分(20条标注测试数据)", + "影响度评估:高/中/低可区分,与标注一致率≥65%", + "全局关键词:高关注/普通两级,包含排除规则同时生效", + "定期报告:Markdown日报/周报自动生成、历史可查,趋势基于≥2个时间周期数据对比" + ], + "sampleData": "模拟信息源HTML/RSS≥3个(覆盖不同风险领域),条目含不同发布日期以支撑趋势;API源附mock脚本" + }, + { + "id": "07", + "title": "需求文档AI评审工具", + "group": "review-tool", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 3, + "funcBreakdown": "文档导入与解析4 / 歧义检测8 / 一致性检查5 / 编写规范检查5 / 可测试性评估4 / 评审报告与规则外置4", + "acceptance": [ + "歧义检测:植入10处歧义词检出≥6处合格线,全检且误报≤2为满分", + "矛盾检测:植入2组矛盾至少检出1组,报告引用双方条目原文", + "可测试性评估:正确标识植入的不可测试需求并附改写建议", + "编写规范检查:基于《需求文档编写标准》配置执行,标准A/B两套切换检出结果变化", + "证据型报告:每个问题定位到章节编号并摘录原文,Critical/Major/Minor分级+通过结论", + "规则外置:检查规则在独立配置文件维护,不改代码可增改" + ], + "sampleData": "2份模拟需求说明书(1正常约10条编号需求;1植入≥20处已知问题)+ 缺陷标注清单 + 标准配置A/B两套" + }, + { + "id": "08", + "title": "设计书AI评审工具", + "group": "review-tool", + "difficulty": 4, + "maxScoreCap": 110, + "minTests": 3, + "funcBreakdown": "导入与解析5 / 规范符合性检查5 / 需求追溯矩阵10 / 接口一致性检查5 / 矩阵可视化与报告5", + "acceptance": [ + "必备章节检查:配置的必备章节缺失全部检出,附应包含内容提示", + "追溯矩阵:10条需求正确建立≥8条映射合格线,未覆盖需求与孤儿设计两类均能标出", + "接口一致性:植入2处矛盾至少检出1处并引用双方定义原文", + "文档格式规范:《设计书格式标准》A/B两套切换检出结果变化", + "证据型报告:分级+定位+通过结论,矩阵表格可视化" + ], + "sampleData": "需求清单1份(10条编号需求)+ 模拟设计书1份(植入章节缺失×2、未覆盖×2、孤儿设计×1、接口矛盾×2、格式违规≥2处)+ 缺陷标注清单 + 格式标准A/B两套" + }, + { + "id": "09", + "title": "代码AI评审工具", + "group": "review-tool", + "difficulty": 3, + "maxScoreCap": 105, + "minTests": 3, + "funcBreakdown": "扫描配置与执行3 / secrets检测6 / LLM调用专项检查7 / 空值防护检查5 / 编码规约检查4 / 报告与门禁5", + "acceptance": [ + "secrets检测:植入8处硬编码密钥检出≥6处合格线,误报≤1为满分", + "LLM调用检查:无保护的LLM调用100%标记,完全无保护Critical/仅缺降级Major", + "空值防护:典型裸访问模式检出率≥70%", + "编码规约:《编码规约》A/B两套切换检出结果变化,条款全部外置不得硬编码", + "证据型报告:定位到 文件名:行号,Critical/Major附修复示例代码,存在Critical输出不通过" + ], + "sampleData": "小型模拟项目代码包(≥10个源码文件,植入硬编码密钥×8、LLM无保护×3、空值裸访问×4、规约违规×4)+ 缺陷标注清单 + 规约A/B两套" + }, + { + "id": "10", + "title": "测试用例AI评审工具", + "group": "review-tool", + "difficulty": 4, + "maxScoreCap": 110, + "minTests": 3, + "funcBreakdown": "测试集导入与解析3 / 需求覆盖分析7 / 断言强度审查9 / 边界完整性提示2 / 基准符合性检查4 / 质量评级报告5", + "acceptance": [ + "覆盖缺口:10条需求中正确找出未覆盖需求≥7条合格线、全部且无误报为满分", + "弱断言检测:植入8个弱断言检出≥5个合格线、≥7个且无误报为满分", + "无断言测试:100%标记并附最小断言补充建议", + "基准符合性:《测试基准》A/B两套切换检出结果变化,覆盖阈值可配置", + "质量评级:每用例A/B/C评级,C级附针对性改进建议" + ], + "sampleData": "需求清单1份(10条编号需求)+ 测试代码集(≥20个用例,植入无断言×3、弱断言×8、要素缺失或命名违规×3)+ 缺陷标注清单 + 基准A/B两套" + }, + { + "id": "11", + "title": "成果物AI评审工具", + "group": "review-tool", + "difficulty": 2, + "maxScoreCap": 100, + "minTests": 3, + "funcBreakdown": "成果物清单配置5 / 存在性核对8 / 要素齐全性检查8 / 交叉一致性检查5 / 准入报告与结论4", + "acceptance": [ + "存在性核对:植入的3处缺失文件全部检出,逐项核对表清晰", + "要素齐全检查:README缺失要素正确检出,同义词扩展命中(安装vs部署)", + "交叉一致性:README技术栈声明 vs 实际依赖文件不一致处检出并引用双方内容", + "清单可切换:加载另一套清单模板正常工作", + "准入结论:Critical缺失(如源代码缺失)直接判定不通过" + ], + "sampleData": "残缺模拟项目提交包(故意缺文件、README缺要素、技术栈声明与依赖不一致)+ 缺陷标注清单 + 备用清单模板一套" + } + ] +} diff --git a/server/config/standards/AI人才育成L2.md b/server/config/standards/AI人才育成L2.md deleted file mode 100644 index 98ed2d5..0000000 --- a/server/config/standards/AI人才育成L2.md +++ /dev/null @@ -1,357 +0,0 @@ -## 功能完整性(40分) - -检查以下5项: - -1. 核心功能实现(12分) - - 题目要求的主要功能全部实现且可运行 → 12分 - - 主要功能实现但有边角缺陷 → 9-11分 - - 部分核心功能实现,其余有框架或占位 → 6-8分 - - 仅有框架无实际功能 → 0分 - -2. 异常处理(8分) - - 有错误提示且覆盖LLM超时/不可达等主要场景 → 8分 - - 有错误提示但不完整,或仅有降级处理之一 → 5-7分 - - 无错误提示但有显式错误反馈 → 3-4分 - - 无任何异常处理 → 0分 - -3. 边界情况(6分) - - 空数据、异常输入均正确处理 → 6分 - - 部分边界场景处理 → 3-5分 - - 无边界处理但程序不崩溃 → 1-2分 - - 边界输入导致崩溃 → 0分 - -4. 业务逻辑正确性(8分) - - 统计结果与手算一致,检索结果相关 → 8分 - - 大部分场景正确,个别边界有误 → 5-7分 - - 存在明显逻辑错误 → 0-4分 - -5. 可运行性(6分) - - README步骤可直接运行,无启动报错 → 6分 - - 需少量额外步骤可运行 → 3-5分 - - 无法运行 → 0分 - -【交叉验证规则】 -- README声称的功能,必须在代码目录中找到对应的实现文件,否则视为未实现 -- 声称"支持异常处理"→代码中必须有try-catch或错误处理逻辑,否则扣4分 -- 声称"支持多种数据格式"→代码中必须有对应的格式处理逻辑,否则扣3分 - -## 设计文档(10分) - -检查以下3项: - -1. 架构图(4分) - - 有架构图或数据流图(若无→0分) - - 图表清晰、标注完整 - -2. 技术选型理由(3分) - - 为什么选择该技术栈(opencode + 课程技术) - - 各组件的作用说明 - -3. 关键设计决策(3分) - - 设计上的取舍和理由 - - 边界情况处理策略 - -## 测试用例与测试结果(10分) - -检查以下3项: - -1. 测试用例数量(3分) - - ≥3个测试用例(若无→0分) - -2. 测试覆盖(4分) - - 覆盖核心逻辑的正常路径和异常路径 - -3. 结果可复现(3分) - - 测试结果附在提交物中 - - 评审者能按步骤复现 - -【交叉验证规则】 -- AGENTS.md或README中声称的"测试覆盖核心功能",必须在代码中找到对应的测试文件(*.spec.ts, *.test.ts, test_*.py等),否则视为造假扣4分 -- 测试文件内容必须有断言(assert/expect/should等),仅有文件骨架而无断言视为无效测试,扣2分 -- 声称"测试全部通过"但测试代码中存在明显语法错误或引用不存在的模块 → 视为造假,该项0分 - -## AI协作过程记录(15分) - -检查以下4项: - -1. AGENTS.md存在(3分) - - 项目根目录有AGENTS.md文件(若无→0分) - -2. prompt记录(5分) - - 记录了使用的prompt原文 - - 记录了生成过程 - -3. 人工修正点(4分) - - 记录了AI生成代码后的人工修改 - - 记录了修改理由 - -4. 问题与对策(3分) - - 记录了遇到的问题 - - 记录了解决方案 - -【交叉验证规则】 -- AGENTS.md中声称的"使用了XX技术",必须在代码中找到对应的配置或调用文件 -- 如果AGENTS.md说"用SpecKit",但代码中没有spec/PRD/TECH_SPEC.md文件 → 视为夸大,扣3分 -- 如果AGENTS.md说"对LLM输出做了质量验证",但代码中没有对应的验证逻辑 → 视为虚构,扣3分 -- 如果AGENTS.md说"编写了测试覆盖核心逻辑",但实际没有测试文件或测试内容与描述不符 → 视为造假,该项0分 - -## 技术选型与范式运用(15分) - -检查以下4项: - -1. 课程技术运用(5分) - - 至少选择了1项指定技术(VibeCoding/Superpowers/SpecKit/Skill/MCP) - - 技术运用方式合理 - -2. 选型理由(4分) - - 说明了为什么选择该技术组合 - - 与题目场景匹配 - -3. 范式运用(3分) - - 运用的范式(SpecKit/VibeCoding等)有明确记录 - - 范式选择与题目特性匹配 - -4. 技术组合深度(3分) - - 组合多项技术的加分 - - 技术之间有明确的调用/协作关系 - -【交叉验证规则】 -- 声称使用了"Skill"→代码中必须有对应的Skill定义文件或注册代码,否则扣3分 -- 声称使用了"SpecKit"→项目中必须有spec/PRD/TECH_SPEC.md文件,否则扣3分 -- 声称使用了"MCP自动化测试"→必须有MCP Server配置或测试脚本调用代码,否则扣3分 -- 声称"组合了多项技术"→必须有明确的调用链路(如A技术调用B技术的代码),否则扣3分 - -## 代码质量 + README(10分) - -检查以下3项: - -1. 代码结构(4分) - - 结构清晰、命名规范 - - 无大量重复代码(重复率>30%扣分) - -2. README完整性(4分) - - 包含:环境要求、安装步骤、运行方法、功能说明 - - README步骤能直接运行成功 - -3. 依赖管理(2分) - - 依赖清单完整 - - 无多余/缺失的依赖 - -【交叉验证规则】 -- README声称的项目功能,必须在代码中有对应的入口文件或模块,否则视为夸大,扣2分 -- README声称的"支持XX平台/环境",必须有对应的配置文件(Dockerfile、.nvmrc等),否则视为不实,扣1分 - -## 第二部分:问题别L3追加维度 - -L3为追加评审,仅在L2合格(共通≥60分)后自动触发。 - - -## [Q2] LLM生成问卷(15分) - -检查以下3项: - -1. 问卷生成逻辑(6分) - - 代码中是否有LLM调用生成面谈问题的逻辑 - - 问卷内容是否与低分维度数据联动(非固定模板文案) - -2. 定制化(5分) - - 是否支持按部门/职级生成不同问题 - - 问卷内容是否可编辑 - -3. 问题质量(4分) - - 生成的问题是否基于具体数据 - - 不是通用模板问题 - -## [Q2] LLM自由文本分析(10分) - -检查以下两项: - -1. 话题分类/摘要(5分) - - 面谈分析页面是否包含AI生成的话题分类结果 - - 是否有自由文本的摘要/分析 - -2. 共性问题提取(5分) - - 是否能跨面谈记录发现高频出现的问题主题 - - 提取的共性问题是否准确 - -## [Q2] prompt工程与调优记录(5分) - -检查以下一项: - -1. prompt调优过程 - - AGENTS.md中是否有prompt调优过程记录 - - 不同策略的对比 - - LLM输出质量验证方法 - - -## [Q3] LLM检索回答(11分) - -检查以下两项: - -1. LLM生成回答(7分) - - 代码中是否有LLM生成回答的逻辑(非单纯关键词匹配) - - 回答是否引用出处文档名/章节/页码 - -2. 置信度显示(4分) - - 回答附带置信度分数 - - 可信度标识清晰 - -## [Q3] 上下文连续对话(10分) - -检查以下两项: - -1. 追问支持(5分) - - 是否支持在上一轮基础上继续提问 - - 上下文理解是否正确 - -2. 对话管理(5分) - - 代码中是否有上下文管理机制 - - 对话状态是否在UI中可见 - -## [Q3] 无法回答处理(10分) - -检查以下两项: - -1. 无法回答检测(5分) - - 对制度文档中不存在的问题,是否明确返回"未在现有制度中找到相关信息" - - 不编造答案 - -2. 合理引导(5分) - - 是否给出合理解释或建议 - - 是否引导用户调整提问方式 - -## [Q3] RAG管道设计记录(19分) - -检查以下4项: - -1. chunk策略设计(5分) - - AGENTS.md是否记录了chunk大小/重叠率选择理由 - - 不同分割策略的对比 - -2. embedding模型选型(5分) - - 是否说明了embedding模型选择理由 - - 是否对比了不同模型的效果 - -3. 检索策略对比(5分) - - 是否记录了关键词/向量/混合检索的对比 - - 检索准确率改善过程 - -4. 多语言处理策略(4分) - - 是否说明了中日文混合检索的处理方案 - - 分词/索引策略的差异处理 - - -## [Q4] LLM条款提取(10分) - -检查以下两项: - -1. 关键字段提取(5分) - - 代码中是否有通过LLM提取合同关键字段的逻辑 - - 提取字段包括:金额、期限、违约责任、管辖法院等 - -2. 提取准确度(5分) - - 审查报告中提取的字段是否正确 - - 是否支持多份合同验证 - -## [Q4] LLM风险标记(10分) - -检查以下两项: - -1. 风险条款标记(5分) - - 是否有AI自动标记风险条款的功能 - - 审查结果页面是否展示风险项 - -2. 风险判断理由(5分) - - 每条风险标记是否附带判断理由 - - 理由是否合理 - -## [Q4] prompt工程与规则设计记录(10分) - -检查以下两项: - -1. 审查规则设计(5分) - - AGENTS.md是否记录了审查规则设计过程 - - 风险标准定义的迭代 - -2. prompt调优(5分) - - prompt调优过程记录 - - 不同策略的对比 - - -## [Q5] LLM内容提取(10分) - -检查以下两项: - -1. 法规字段提取(5分) - - 代码中是否有AI解析网页内容提取法规字段的逻辑 - - 提取字段包括:法规名称、发布机关、发布日期、摘要 - -2. 提取质量(5分) - - 提取结果是否准确 - - 是否支持不同的网页结构 - -## [Q5] LLM影响度评估(10分) - -检查以下两项: - -1. 影响度自动判断(5分) - - 是否有AI自动评估高/中/低影响度的功能 - - 判断依据是否来源于LLM分析 - -2. 影响度展示(5分) - - 影响度在结果列表中是否清晰标识 - - 高影响度是否突出显示 - -## [Q5] prompt工程与爬虫策略记录(10分) - -检查以下两项: - -1. 爬虫策略设计(5分) - - AGENTS.md是否记录了爬虫规则设计 - - 反爬策略 - -2. 提取准确率改善(5分) - - 提取准确率的改善过程 - - 不同网站结构的处理策略 - - -## [Q6] LLM风险分类(10分) - -检查以下两项: - -1. 自动分类逻辑(5分) - - 代码中是否有AI自动将情报按领域分类的逻辑 - - 分类维度是否与预设一致(汇率/法规/日中关系/行业/技术) - -2. 分类展示(5分) - - 分类结果在界面中是否清晰展示 - - 是否支持按分类筛选 - -## [Q6] LLM影响度评估(10分) - -检查以下两项: - -1. 风险等级评估(5分) - - 是否有AI根据内容评估风险等级的功能 - - 评估依据是否可追溯 - -2. 高风险处理(5分) - - 高风险情报是否自动突出显示 - - 是否有通知机制(系统内/模拟邮件) - -## [Q6] prompt工程与分类策略记录(10分) - -检查以下两项: - -1. 分类模型选择(5分) - - AGENTS.md是否记录了分类模型选择理由 - - 影响度评估标准定义 - -2. 策略调优(5分) - - 通知规则设计 - - 分类准确率改善过程 - -## 合格判定一览 - -| 题目 | 难度 | 共通满分 | 追加满分 | 总分上限 | L2合格 | L3合格 | -|:----|:----:|:--------:|:--------:|:--------:|:------:|:--- \ No newline at end of file diff --git a/server/config/standards/L2考核-评审标准.md b/server/config/standards/L2考核-评审标准.md new file mode 100644 index 0000000..8b23ad1 --- /dev/null +++ b/server/config/standards/L2考核-评审标准.md @@ -0,0 +1,79 @@ +## 功能完整性(30分) + +评审基准: +- 命题题:按系统注入的该题验收基准表逐项评定,未实现或与声明不符的功能不计分 +- 自选题:按 README 声明的功能清单逐项核对实现情况(结合登记功能清单快照,低于登记范围核心功能的缩减部分按未实现计) + +分档锚点: +- 核心功能全部实现且实测可运行:25-30分 +- 主要功能实现,个别非核心功能缺失或有边角缺陷:18-24分 +- 部分核心功能实现,存在占位/框架代码:10-17分 +- 仅框架无实际功能,或主要功能无法运行:0-9分 + +【交叉验证规则】 +- README 声称的功能,必须在代码目录中找到对应的实现入口/模块,否则视为未实现 +- 声称"支持异常处理"→代码中必须有 try-catch 或错误处理逻辑,否则扣4分 +- 声称"支持多种数据格式"→代码中必须有对应的格式处理逻辑,否则扣3分 +- 凡涉及 LLM/API 调用的功能,必须具备超时设置、异常捕获、降级或显式错误提示至少一项;全部缺失扣4分 + +## 设计文档(10分) + +1. 架构图(4分):有架构图或数据流图,模块划分与外部依赖(LLM API 等)标注清晰;无图计0-1分 +2. 开发范式流程图(3分):本人开发过程的工作流设计;图中步骤名称与 _AI_USAGE_LOG.md 的「范式步骤」列逐步对应,对应不上酌情扣分 +3. 关键设计决策(3分):重要取舍有理由;评分型指标(置信度/影响度/准确率/高频阈值等)的计算口径必须在设计文档中写明,未写明口径扣2分 + +## 测试用例与测试结果(10分) + +1. 用例数量(3分):至少3个测试用例(命题题从其题目约定);无测试文件计0分 +2. 覆盖质量(4分):覆盖核心逻辑的正常路径与异常路径;断言真实有效——仅有文件骨架而无断言(assert/expect/should)视为无效测试 +3. 结果可复现(3分):测试以命令行方式可执行,执行结果附于 tests/ 或 README;评审系统将实际运行测试并与声明对照 + +【交叉验证规则】 +- 声称"测试覆盖核心功能",必须找到对应测试文件(*.spec.ts / *.test.ts / test_*.py 等),否则视为造假扣4分 +- 声称"测试全部通过"但测试代码存在明显语法错误或引用不存在的模块 → 该项0分 +- 评审环境导致的测试失败(依赖缺失等)为中性证据,不据此扣分,但受验者声明的结果与实测矛盾时扣分 + +## AI协作过程记录(15分) + +1. AGENTS.md 存在(3分):项目根目录有 AGENTS.md;缺失计0分 +2. prompt 原文(5分):记录核心功能生成过程的 prompt 原文,可追溯到具体功能 +3. 人工修正点(4分):记录 AI 输出后自己改了什么、为什么要改 +4. 问题与对策(3分):记录遇到的问题(幻觉API、超时缺失等)与解决方案 + +【交叉验证规则】 +- AGENTS.md 声称"使用了XX技术",必须在代码中找到对应配置或调用文件 +- 说"用 SpecKit"但没有 spec/PRD/TECH_SPEC 类文件 → 视为夸大扣3分 +- 说"做了LLM输出质量验证"但没有对应验证逻辑 → 视为虚构扣3分 +- _AI_USAGE_LOG.md 声称的修改所涉及的文件路径,须在 git 历史中存在对应变更;凭空捏造的日志条目每处扣2分 + +## 技术选型与范式运用(15分) + +1. 课程技术运用(5分):VibeCoding / Superpowers / SpecKit / Skill / MCP自动化测试 至少1项真实运用于开发过程,运用深度合理(非表面套用);一项都没有计0分 +2. 选型理由(4分):说明了为何选择该技术组合、与题目场景的匹配性 +3. 范式运用记录(3分):运用的范式有明确记录(AGENTS.md 课程技术运用节) +4. 组合深度(3分):组合多项技术时有明确的调用/协作关系 + +【交叉验证规则】 +- 声称 Skill → 代码中必须有 Skill 定义文件或注册代码,否则扣3分 +- 声称 MCP 自动化测试 → 必须有 MCP Server 配置或测试脚本调用代码,否则扣3分 +- 声称"组合多项技术"→ 必须有明确调用链路(A技术调用B技术的代码),否则扣3分 + +## 代码质量+README(10分) + +1. 代码结构(4分):结构清晰、命名规范、无硬编码密钥/绝对路径;文件级重复率超过30%酌情扣分,超过50%本项计0-1分 +2. README 完整性(4分):含痛点背景、功能说明、效果总结、安装运行四部分,安装步骤逐步可复制执行;缺一部分扣1分 +3. 依赖管理(2分):依赖清单/锁文件完整可复现,无多余缺失依赖 + +【交叉验证规则】 +- README 声称的项目功能必须有对应入口文件或模块,否则视为夸大扣2分 +- 声称"支持XX平台/环境"但无对应配置文件(Dockerfile、.nvmrc 等)→ 扣1分 + +## 业务场景理解与需求分析(10分) + +1. 痛点理解(4分):对业务场景痛点理解准确,目标用户/使用场景清晰(命题题为该题业务场景的理解分析,自选题必须有现状低效点分析) +2. 需求分析(3分):功能边界清楚,需求拆解合理 +3. 效果总结可信(3分):提效数字附测量方式(如何计时、对比场景与步骤);基于模拟场景的小样本试用如实记录即可;无法量化的说明具体痛点和实际反馈 + +【真实性规则】 +- 无测量依据的夸大数字在可信度上扣分(本项计0-1分) +- 文档声称效果与实测明显不符 → 本项计0分并在评语标注 diff --git a/server/src/__tests__/api.test.ts b/server/src/__tests__/api.test.ts index 6f43ff3..a13f1f9 100644 --- a/server/src/__tests__/api.test.ts +++ b/server/src/__tests__/api.test.ts @@ -448,73 +448,75 @@ describe('Cookie Auth(K7 回归)', () => { }); }); -describe('Admin Correction: cap + final_level(K3 回归)', () => { +describe('Admin Correction: cap + L2 final_level(整合后回归)', () => { let pid = ''; let eid = ''; beforeAll(async () => { - const proj = await api('POST', '/api/projects', { name: 'report-cap-test', track: '人才测评' }); + const proj = await api('POST', '/api/projects', { name: 'report-cap-test', track: 'L2考核' }); pid = proj.data.id; - // 删除项目自动生成的默认标准,替换为自定义 Q2 标准 const list = await api('GET', `/api/projects/${pid}/standards`); for (const s of list.data) { await api('DELETE', `/api/projects/${pid}/standards/${s.id}`); } + // L2 7维标准:功能30+设计10+测试10+AI协作15+技术选型15+代码+README10+业务场景10=100 await api('POST', `/api/projects/${pid}/standards`, { - name: 'rc-std', category_tag: '人才测评', - content: '## 功能完整性(40分)\n## 设计文档(10分)\n## [Q2] LLM生成问卷(15分)\n## [Q2] LLM自由文本分析(10分)', + name: 'rc-std', category_tag: 'L2考核', + content: '## 功能完整性(30分)\n## 设计文档(10分)\n## 测试用例与测试结果(10分)\n## AI协作过程记录(15分)\n## 技术选型与范式运用(15分)\n## 代码质量+README(10分)\n## 业务场景理解与需求分析(10分)', }); const entry = await api('POST', `/api/projects/${pid}/entries`, { - title: 'cap-entry', repo_url: `file://C:\\cap-${Date.now()}`, question_id: 'Q2', + title: 'cap-entry', repo_url: `file://C:\\cap-${Date.now()}`, selected_topic: 'self', }); eid = entry.data.id; }); afterAll(async () => { await api('DELETE', `/api/projects/${pid}?force=true`); }); - it('TC-REPORT-01: 人工修正后 final_level 按新分数重算(L3→不合格→L3)', async () => { + it('TC-REPORT-01: L2 人工修正后 final_level 按合格/不合格重算(含功能完整性地板线)', async () => { const full = [ - { name: '功能完整性', score: 40, maxScore: 40, group: 'common' }, - { name: '设计文档', score: 10, maxScore: 10, group: 'common' }, - { name: 'LLM生成问卷', score: 15, maxScore: 15, group: 'Q2' }, - { name: 'LLM自由文本分析', score: 10, maxScore: 10, group: 'Q2' }, + { name: '功能完整性', score: 30, maxScore: 30 }, + { name: '设计文档', score: 10, maxScore: 10 }, + { name: '测试用例与测试结果', score: 10, maxScore: 10 }, + { name: 'AI协作过程记录', score: 15, maxScore: 15 }, + { name: '技术选型与范式运用', score: 15, maxScore: 15 }, + { name: '代码质量+README', score: 10, maxScore: 10 }, + { name: '业务场景理解与需求分析', score: 10, maxScore: 10 }, ]; const fr = await api('PUT', `/api/projects/${pid}/entries/${eid}/force-review`, { dimensions: full }); expect(fr.status).toBe(200); - // force-review 仅播种 ai_report;final_level 由 PUT /report 重算 const seed = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(seed.status).toBe(200); let detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); - expect(detail.data.final_level).toBe('L3'); // L2 50/50 达标,总 75/75 ≥0.8 + expect(detail.data.final_level).toBe('合格'); // 总分100 ≥60 且 功能30 ≥15 - const lowCommon = [ - { name: '功能完整性', score: 15, maxScore: 40, group: 'common' }, - { name: '设计文档', score: 5, maxScore: 10, group: 'common' }, - { name: 'LLM生成问卷', score: 15, maxScore: 15, group: 'Q2' }, - { name: 'LLM自由文本分析', score: 10, maxScore: 10, group: 'Q2' }, - ]; - const fix = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: lowCommon }); + // 功能完整性低于地板线15 → 即使总分高也判不合格 + const lowFunc = full.map(d => d.name === '功能完整性' ? { ...d, score: 10 } : d); + const fix = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: lowFunc }); expect(fix.status).toBe(200); detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); - expect(detail.data.final_level).toBe('不合格'); // L2 20/50 < pass 30 + expect(detail.data.final_level).toBe('不合格'); // 功能10 < 15 地板线 + // 恢复 const fix2 = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(fix2.status).toBe(200); detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); - expect(detail.data.final_level).toBe('L3'); + expect(detail.data.final_level).toBe('合格'); }); it('TC-REPORT-02: 人工修正应用 max_score_cap 后再扣迟交', async () => { const db = (await import('../db')).default; db.prepare('UPDATE entries SET max_score_cap = 30, late_days = 0 WHERE id = ?').run(eid); const full = [ - { name: '功能完整性', score: 40, maxScore: 40, group: 'common' }, - { name: '设计文档', score: 10, maxScore: 10, group: 'common' }, - { name: 'LLM生成问卷', score: 15, maxScore: 15, group: 'Q2' }, - { name: 'LLM自由文本分析', score: 10, maxScore: 10, group: 'Q2' }, + { name: '功能完整性', score: 30, maxScore: 30 }, + { name: '设计文档', score: 10, maxScore: 10 }, + { name: '测试用例与测试结果', score: 10, maxScore: 10 }, + { name: 'AI协作过程记录', score: 15, maxScore: 15 }, + { name: '技术选型与范式运用', score: 15, maxScore: 15 }, + { name: '代码质量+README', score: 10, maxScore: 10 }, + { name: '业务场景理解与需求分析', score: 10, maxScore: 10 }, ]; const fix = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(fix.status).toBe(200); const detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); - expect(detail.data.raw_score).toBe(75); - expect(detail.data.final_score).toBe(30); // min(75, cap30) - 0 + expect(detail.data.raw_score).toBe(100); + expect(detail.data.final_score).toBe(30); // min(100, cap30) - 0 db.prepare('UPDATE entries SET max_score_cap = 100 WHERE id = ?').run(eid); }); }); diff --git a/server/src/__tests__/build-status.test.ts b/server/src/__tests__/build-status.test.ts index 8273d99..7a8afad 100644 --- a/server/src/__tests__/build-status.test.ts +++ b/server/src/__tests__/build-status.test.ts @@ -39,7 +39,7 @@ afterAll(async () => { server?.close(); }); -describe('build_status(赛道二/人才测评 单阶段人工构建确认,§2.2 扩展)', () => { +describe('build_status(赛道二/L2考核 单阶段人工构建确认,§2.2 扩展)', () => { it('创建条目可带 build_status=done/failed,并持久化', async () => { const r = await api('POST', `/api/projects/${projectId}/entries`, { title: 'bs-done', repo_url: `file://D:\\bs-${Date.now()}`, build_status: 'done', diff --git a/server/src/__tests__/plagiarism.test.ts b/server/src/__tests__/plagiarism.test.ts new file mode 100644 index 0000000..32085cc --- /dev/null +++ b/server/src/__tests__/plagiarism.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { + detectPlagiarism, + detectCommitBehavior, + readRepoFiles, + PLAG_SIM_THRESHOLD, +} from '../services/plagiarism-detect'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +function mkFile(dir: string, rel: string, content: string) { + const full = path.join(dir, rel); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, content); +} + +describe('TC-PLAG · L2 查重初筛(§10)', () => { + it('相同文件(MD5 一致)→ identicalFile flag', () => { + const a = readRepoFiles('A'); + const b = readRepoFiles('B'); + void a; void b; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plag-')); + const d1 = path.join(tmp, 'A'); const d2 = path.join(tmp, 'B'); + mkFile(d1, 'src/main.py', 'def solve(x):\n return x * 2\n'); + mkFile(d1, 'README.md', '# 项目A\n'); + mkFile(d2, 'src/main.py', 'def solve(x):\n return x * 2\n'); // 完全相同 + mkFile(d2, 'README.md', '# 项目B\n'); + const r = detectPlagiarism('A', readRepoFiles(d1), { B: readRepoFiles(d2) }); + expect(r.flags.some(f => f.kind === 'identicalFile')).toBe(true); + const f = r.flags.find(f => f.kind === 'identicalFile')!; + expect(f.samples.some(s => s.file === 'src/main.py' && s.otherFile === 'src/main.py')).toBe(true); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('不同代码 → 无 flag', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plag-')); + const d1 = path.join(tmp, 'A'); const d2 = path.join(tmp, 'B'); + mkFile(d1, 'src/main.py', 'def solve(x):\n return x + 1\n'); + mkFile(d2, 'src/main.py', 'def other(y):\n return y * 3 - 7\n'); + const r = detectPlagiarism('A', readRepoFiles(d1), { B: readRepoFiles(d2) }); + expect(r.flags.some(f => f.kind === 'identicalFile')).toBe(false); + expect(r.flags.some(f => f.kind === 'highSimilarity')).toBe(false); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('高相似同名源码 → highSimilarity flag(阈值由 PLAG_SIM_THRESHOLD 控制)', () => { + expect(PLAG_SIM_THRESHOLD).toBeGreaterThanOrEqual(0.5); + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plag-')); + const d1 = path.join(tmp, 'A'); const d2 = path.join(tmp, 'B'); + // 仅改一行注释/变量名,其余完全一致 → 归一化后几乎相同 + mkFile(d1, 'src/util.ts', 'export function calc(a: number, b: number): number {\n const sum = a + b;\n if (sum > 100) return sum;\n return sum;\n}\n'); + mkFile(d2, 'src/util.ts', 'export function calc(a: number, b: number): number {\n const sum = a + b;\n if (sum > 100) return sum;\n return sum;\n}\n'); + const r = detectPlagiarism('A', readRepoFiles(d1), { B: readRepoFiles(d2) }); + // 完全相同会同时触发 identicalFile;此处关注 highSimilarity 也能被标记 + expect(r.flags.some(f => f.kind === 'highSimilarity' || f.kind === 'identicalFile')).toBe(true); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('忽略依赖/脚手架白名单文件', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plag-')); + const d1 = path.join(tmp, 'A'); const d2 = path.join(tmp, 'B'); + mkFile(d1, 'package.json', '{"name":"x","dependencies":{"lodash":"^4"}}'); + mkFile(d1, 'package-lock.json', '{"lockfileVersion":3}'); + mkFile(d2, 'package.json', '{"name":"x","dependencies":{"lodash":"^4"}}'); + mkFile(d2, 'package-lock.json', '{"lockfileVersion":3}'); + const r = detectPlagiarism('A', readRepoFiles(d1), { B: readRepoFiles(d2) }); + expect(r.flags.length).toBe(0); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('commit 行为:单次全量提交 → suspiciousCommit;多次提交 → 无', () => { + const bulk = detectCommitBehavior('A', 1, true); + expect(bulk).not.toBeNull(); + expect(bulk!.kind).toBe('suspiciousCommit'); + const ok = detectCommitBehavior('A', 12, false); + expect(ok).toBeNull(); + }); +}); diff --git a/server/src/__tests__/standard-utils.test.ts b/server/src/__tests__/standard-utils.test.ts index c2f4e7b..7a3de2a 100644 --- a/server/src/__tests__/standard-utils.test.ts +++ b/server/src/__tests__/standard-utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { computeCommonTotal, computeMaxBonus, computeEffectiveTotal, computePassLine, matchDimKey, computeFinalLevel, computeLatePenalty, computeCalibration, parseDimResponse, resolveSubmitTime, computeLateDays, aggregateScores, aggregateEntryScores, classifyVerifiability, isEffectDim, detectStructuralContradictions, neutralizeTestEvidence } from '../services/standard-utils'; +import { computeCommonTotal, computeMaxBonus, computeEffectiveTotal, computePassLine, matchDimKey, computeLatePenalty, computeCalibration, parseDimResponse, resolveSubmitTime, computeLateDays, aggregateScores, aggregateEntryScores, classifyVerifiability, isEffectDim, detectStructuralContradictions, neutralizeTestEvidence } from '../services/standard-utils'; const dims = [ { name: '共通A', maxScore: 40, group: 'common' }, @@ -22,8 +22,8 @@ describe('TC-STDUTIL · 标准工具(§3.3.1 / §3.3.8)', () => { expect(computeEffectiveTotal(dims)).toBe(95); }); - it('computePassLine: 人才测评 = round(共通×0.6)', () => { - expect(computePassLine(dims, '人才测评')).toBe(42); + it('computePassLine: L2考核 与共通赛道一致 = round(有效总分×0.6)', () => { + expect(computePassLine(dims, 'L2考核')).toBe(57); }); it('computePassLine: 其他赛道 = round(有效总分×0.6)', () => { @@ -46,35 +46,6 @@ describe('TC-STDUTIL · 标准工具(§3.3.1 / §3.3.8)', () => { }); }); -describe('TC-L2L3 · computeFinalLevel(§3.3.8 / K3)', () => { - const mk = (score: number, maxScore: number, group = 'common') => ({ score, maxScore, group }); - - it('共通达标 + L3 得分率≥0.8 → L3', () => { - const dims = [mk(40, 40, 'common'), mk(10, 10, 'common'), mk(15, 15, 'Q2'), mk(10, 10, 'Q2')]; - expect(computeFinalLevel(dims, 30)).toBe('L3'); // (50+25)/75 = 1.0 - }); - - it('共通达标 + L3 得分率<0.8 → L2', () => { - const dims = [mk(40, 40, 'common'), mk(10, 10, 'common'), mk(5, 15, 'Q2'), mk(2, 10, 'Q2')]; - expect(computeFinalLevel(dims, 30)).toBe('L2'); // (50+7)/75 ≈ 0.76 - }); - - it('共通未达标 → 不合格', () => { - const dims = [mk(15, 40, 'common'), mk(5, 10, 'common'), mk(15, 15, 'Q2')]; - expect(computeFinalLevel(dims, 30)).toBe('不合格'); // L2 20 < 30 - }); - - it('无 L3 维度:共通达标 → L2', () => { - const dims = [mk(40, 40, 'common'), mk(10, 10, 'common')]; - expect(computeFinalLevel(dims, 30)).toBe('L2'); - }); - - it('passLine 为 0 时回退 round(共通满分×0.6)', () => { - const dims = [mk(30, 50, 'common')]; - expect(computeFinalLevel(dims, 0)).toBe('L2'); // 30 >= round(50×0.6)=30 - }); -}); - describe('TC-LATE · computeLatePenalty(§3.3.9)', () => { it('lateDays≤0 → 不扣', () => { expect(computeLatePenalty(100, 0, 5)).toBe(0); diff --git a/server/src/__tests__/standards.test.ts b/server/src/__tests__/standards.test.ts index 8d4deaf..a47f788 100644 --- a/server/src/__tests__/standards.test.ts +++ b/server/src/__tests__/standards.test.ts @@ -83,7 +83,7 @@ describe('parseDimensions', () => { expect(result.map(d => d.name)).toEqual(['评审维度设计', '功能完整性']); }); - it('should infer Qn group from "N-A." prefix (人才测评追加维度)', () => { + it('should infer Qn group from "N-A." prefix', () => { const md = [ '## 功能完整性(40分)', '共通', diff --git a/server/src/__tests__/track2-pure.test.ts b/server/src/__tests__/track2-pure.test.ts index 8c35091..7594f74 100644 --- a/server/src/__tests__/track2-pure.test.ts +++ b/server/src/__tests__/track2-pure.test.ts @@ -314,7 +314,7 @@ describe('isBuildRelatedDim(构建封顶维度识别,含功能完整性盲 expect(isBuildRelatedDim('实现完整度')).toBe(true); }); - it('修复 功能完整性 盲区(人才测评 L2)', () => { + it('识别 功能完整性(L2 构建封顶维度)', () => { expect(isBuildRelatedDim('功能完整性')).toBe(true); }); diff --git a/server/src/db.ts b/server/src/db.ts index 9d982fe..3d0e5f3 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -98,12 +98,17 @@ try { db.exec("ALTER TABLE entries ADD COLUMN score_a REAL DEFAULT 0"); } catch try { db.exec("ALTER TABLE entries ADD COLUMN score_b REAL DEFAULT 0"); } catch (e) {} try { db.exec("ALTER TABLE entries ADD COLUMN stage_b_status TEXT DEFAULT ''"); } catch (e) {} try { db.exec("ALTER TABLE entries ADD COLUMN project_understanding TEXT DEFAULT ''"); } catch (e) {} -// 赛道二/人才测评 单阶段人工构建确认(2026-08-18):''=未确认(自动构建) / done / failed +// 赛道二/L2考核 单阶段人工构建确认(2026-08-18):''=未确认(自动构建) / done / failed try { db.exec("ALTER TABLE entries ADD COLUMN build_status TEXT DEFAULT ''"); } catch (e) {} // 多次评审聚合(2026-08-19):快照分数列(含迟交扣分),排名用稳健统计 try { db.exec("ALTER TABLE review_snapshots ADD COLUMN score REAL"); } catch (e) {} // 决赛圈基准证据(2026-08-19):按 entry 落库,防并发 env 串数据 try { db.exec("ALTER TABLE entries ADD COLUMN benchmark_json TEXT DEFAULT ''"); } catch (e) {} +// L2考核(2026-08-23):选题编号(01~11/self)与自选题登记(JSON:登记编号+功能清单快照) +try { db.exec("ALTER TABLE entries ADD COLUMN selected_topic TEXT DEFAULT ''"); } catch (e) {} +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) {} // 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`); diff --git a/server/src/routes/entries.ts b/server/src/routes/entries.ts index f00ab5c..647f5a5 100644 --- a/server/src/routes/entries.ts +++ b/server/src/routes/entries.ts @@ -8,12 +8,13 @@ import db from '../db'; import { config } from '../config'; import { isPathInside } from '../path-security'; import { parseDimensions } from './standards'; -import { computePassLine, computeFinalLevel, computeLatePenalty, aggregateEntryScores } from '../services/standard-utils'; +import { computePassLine, computeLatePenalty, aggregateEntryScores, computeL2Result } from '../services/standard-utils'; import { isPrivateAddress } from '../ip-security'; import { REVIEW_CONSTANTS } from '../services/review-constants'; import { startReview, startReviewB, resolveWebMode, averageDimensions } from '../services/review.service'; import { generateEntryPdf } from '../services/pdf.service'; import { resolveRepoUrlFromConfig } from '../services/teams-config'; +import { findL2Topic } from '../services/l2-topics'; const router = Router({ mergeParams: true }); @@ -176,7 +177,7 @@ router.post('/', async (req: Request, res: Response) => { const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(pid(req)) as any; if (!project) return res.status(404).json({ error: '项目不存在' }); - const { title, repo_url, participant, branch, service_url, base_branch, sub_type, question_id, build_status } = req.body; + const { title, repo_url, participant, branch, service_url, base_branch, sub_type, question_id, build_status, selected_topic, self_registration } = req.body; if (!title?.trim()) return res.status(400).json({ error: '标题为必填项' }); if (build_status !== undefined && build_status !== '' && build_status !== 'done' && build_status !== 'failed') { return res.status(400).json({ error: '构建结果必须为 done 或 failed' }); @@ -195,11 +196,23 @@ router.post('/', async (req: Request, res: Response) => { const subType = track === '赛道一' ? (sub_type || '') : ''; const categoryTag = track || ''; - // 人才测评 must have question_id - if (track === '人才测评' && !question_id) { - return res.status(400).json({ error: '人才测评条目必须选择题目' }); + // L2考核:选题(默认自选题)决定难度赋分与 max_score_cap;自选题登记随条目落库 + let l2TopicId = ''; + let l2Cap = 100; + let selfRegJson = ''; + if (track === 'L2考核') { + l2TopicId = selected_topic || 'self'; + const t = findL2Topic(l2TopicId); + if (!t) return res.status(400).json({ error: '无效的选题编号' }); + l2Cap = t.maxScoreCap; + if (l2TopicId === 'self' && self_registration !== undefined && self_registration !== null) { + try { + selfRegJson = typeof self_registration === 'string' + ? JSON.stringify(JSON.parse(self_registration)) + : JSON.stringify(self_registration); + } catch { selfRegJson = ''; } + } } - const qid = track === '人才测评' ? question_id : ''; const standard = resolveStandard(pid(req), track, subType); if (!standard) return res.status(400).json({ error: '未找到匹配的评审标准,请先上传标准' }); @@ -209,11 +222,11 @@ router.post('/', async (req: Request, res: Response) => { const id = crypto.randomUUID(); try { - db.prepare(`INSERT INTO entries (id, project_id, standard_id, title, repo_url, category_tag, sub_type, question_id, participant, pass_line, standard_snapshot, branch, service_url, base_branch, build_status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + db.prepare(`INSERT INTO entries (id, project_id, standard_id, title, repo_url, category_tag, sub_type, question_id, participant, pass_line, standard_snapshot, branch, service_url, base_branch, build_status, selected_topic, self_registration, max_score_cap) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( id, pid(req), standard.id, title.trim(), resolvedRepoUrl, - categoryTag, subType, qid, participant || '', passLine, JSON.stringify(dims), branch?.trim() || '', (service_url || '').trim(), - (base_branch || '').trim(), (build_status || '').trim() + categoryTag, subType, '', participant || '', passLine, JSON.stringify(dims), branch?.trim() || '', (service_url || '').trim(), + (base_branch || '').trim(), (build_status || '').trim(), l2TopicId, selfRegJson, l2Cap ); } catch (err: any) { if (err.message?.includes('UNIQUE constraint')) { @@ -248,8 +261,6 @@ router.post('/batch', async (req: Request, res: Response) => { const subType = track === '赛道一' ? (item.sub_type || '') : ''; const categoryTag = track || ''; - const qid = track === '人才测评' ? (item.question_id || '') : ''; - if (track === '人才测评' && !qid) { errors.push({ row: i, reason: '人才测评必须选择题目' }); continue; } const standard = resolveStandard(pid(req), track, subType); if (!standard) { errors.push({ row: i, reason: '未找到匹配的标准' }); continue; } @@ -261,7 +272,7 @@ router.post('/batch', async (req: Request, res: Response) => { db.prepare(`INSERT INTO entries (id, project_id, standard_id, title, repo_url, category_tag, sub_type, question_id, participant, pass_line, standard_snapshot, branch, service_url, base_branch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( id, pid(req), standard.id, item.title.trim(), resolvedRepoUrl, - categoryTag, subType, qid, item.participant || '', passLine, JSON.stringify(dims), + categoryTag, subType, '', item.participant || '', passLine, JSON.stringify(dims), item.branch?.trim() || '', (item.service_url || '').trim(), (item.base_branch || '').trim() ); @@ -285,7 +296,7 @@ router.put('/:entryId', async (req: Request, res: Response) => { // §2.5.1 放开:允许 pending / a_done 编辑(service_url 是 a_done 的核心用途,B 阶段前置条件) if (existing.status !== 'pending' && existing.status !== 'a_done') return res.status(409).json({ error: '只能编辑待评审或等待系统验证的条目' }); - const { title, repo_url, participant, branch, service_url, base_branch, sub_type, question_id, build_status } = req.body; + const { title, repo_url, participant, branch, service_url, base_branch, sub_type, question_id, build_status, selected_topic, self_registration } = req.body; if (service_url !== undefined && service_url) { const urlCheck = await validateServiceUrl(service_url); if (!urlCheck.valid) return res.status(400).json({ error: `服务地址无效: ${urlCheck.reason}` }); @@ -296,9 +307,29 @@ router.put('/:entryId', async (req: Request, res: Response) => { const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(pid(req)) as any; const track = project?.track || ''; const subTypeVal = track === '赛道一' && sub_type !== undefined ? sub_type : existing.sub_type; - const qidVal = track === '人才测评' && question_id !== undefined ? question_id : existing.question_id; + const qidVal = existing.question_id || ''; - db.prepare(`UPDATE entries SET title=?, repo_url=?, sub_type=?, question_id=?, participant=?, branch=?, service_url=?, base_branch=?, build_status=?, updated_at=datetime('now') WHERE id=?`).run( + // L2考核:选题/登记更新时同步 max_score_cap + let selTopicVal = existing.selected_topic || ''; + let selfRegVal = existing.self_registration || ''; + let capVal: number | undefined; + if (track === 'L2考核') { + if (selected_topic !== undefined) { + const t = findL2Topic(selected_topic || 'self'); + if (!t) return res.status(400).json({ error: '无效的选题编号' }); + selTopicVal = selected_topic || 'self'; + capVal = t.maxScoreCap; + } + if (self_registration !== undefined) { + try { + selfRegVal = typeof self_registration === 'string' + ? JSON.stringify(JSON.parse(self_registration)) + : JSON.stringify(self_registration); + } catch { selfRegVal = ''; } + } + } + + db.prepare(`UPDATE entries SET title=?, repo_url=?, sub_type=?, question_id=?, participant=?, branch=?, service_url=?, base_branch=?, build_status=?, selected_topic=?, self_registration=?, max_score_cap=COALESCE(?, max_score_cap), updated_at=datetime('now') WHERE id=?`).run( title?.trim() || existing.title, repo_url?.trim() || existing.repo_url, subTypeVal, @@ -308,6 +339,9 @@ router.put('/:entryId', async (req: Request, res: Response) => { service_url !== undefined ? (service_url || '').trim() : (existing.service_url || ''), base_branch !== undefined ? (base_branch || '').trim() : (existing.base_branch || ''), build_status !== undefined ? (build_status || '').trim() : (existing.build_status || ''), + selTopicVal, + selfRegVal, + capVal ?? null, eid(req) ); const updated = db.prepare('SELECT * FROM entries WHERE id = ?').get(eid(req)); @@ -558,12 +592,22 @@ router.put('/:entryId/report', (req: Request, res: Response) => { const proj = db.prepare('SELECT late_penalty FROM projects WHERE id = ?').get(pid(req)) as any; penalty = computeLatePenalty(totalScore, entry.late_days, proj?.late_penalty ?? REVIEW_CONSTANTS.DEFAULT_LATE_PENALTY); } - // 与评审管线对齐:应用 max_score_cap 上限后再扣迟交分 + // 与评审管线对齐:应用 max_score_cap 上限后再扣迟交分(L2 额外应用难度赋分并封顶选题 cap) const cappedScore = entry.max_score_cap && entry.max_score_cap < 100 ? Math.min(totalScore, entry.max_score_cap) : totalScore; - const finalScore = Math.max(0, Math.round(cappedScore - penalty)); + const projInfo = db.prepare('SELECT track FROM projects WHERE id = ?').get(pid(req)) as any; + let finalScore = Math.max(0, Math.round(cappedScore - penalty)); + if (projInfo?.track === 'L2考核') { + const t = findL2Topic(entry.selected_topic || 'self'); + const bonus = t ? Math.max(0, t.difficulty - 2) * 5 : 0; + const cap = t ? t.maxScoreCap : 100; + finalScore = Math.min(cap, Math.max(0, Math.round(cappedScore + bonus - penalty))); + } - // 人才测评:按修正后的维度重算 final_level(与评审管线共用纯函数,K3) - const finalLevel = entry.question_id ? computeFinalLevel(updatedDims, entry.pass_line || 0) : (entry.final_level || ''); + // 修正后的 final_level:L2考核用 computeL2Result(合格/不合格),其余保留原值 + let finalLevel = entry.final_level || ''; + if (projInfo?.track === 'L2考核') { + finalLevel = computeL2Result(finalScore, updatedDims); + } db.prepare("UPDATE entries SET ai_report = ?, raw_score = ?, final_score = ?, final_level = ?, status = 'admin_reviewed', updated_at = datetime('now') WHERE id = ?").run( JSON.stringify(updatedReport), totalScore, finalScore, finalLevel, eid(req)); diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index ce7d61c..2100527 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -7,9 +7,15 @@ import { config } from '../config'; import { parseDimensions } from './standards'; import { computeEffectiveTotal, aggregateEntryScores } from '../services/standard-utils'; import { generateSummaryPdf } from '../services/pdf.service'; +import { getL2Topics } from '../services/l2-topics'; const router = Router(); +// L2考核选题元数据(前端选题下拉 / 评审注入同源) +router.get('/l2-topics', (_req: Request, res: Response) => { + res.json(getL2Topics()); +}); + function getProjectOr404(id: string, res: Response) { const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(id); if (!project) { res.status(404).json({ error: '项目不存在' }); return null; } @@ -50,8 +56,8 @@ function buildSummary(projectId: string) { const byCategory: Record = {}; for (const { entry: e, score, aggregate_count, is_formal } of enriched) { - // 人才测评按 question_id 分组,其他按 category_tag 分组 - const tag = (e.category_tag === '人才测评' && e.question_id) ? e.question_id : (e.category_tag || '未分类'); + // 统一按 category_tag 分组 + const tag = e.category_tag || '未分类'; if (!byCategory[tag]) byCategory[tag] = []; byCategory[tag].push({ e, score, aggregate_count, is_formal }); } @@ -107,7 +113,7 @@ router.get('/', (_req: Request, res: Response) => { const DEFAULT_STANDARDS: Record = { '赛道一': { name: '技术大赛·赛道一标准', file: '技术大赛-赛道一.md' }, '赛道二': { name: '技术大赛·赛道二标准', file: '技术大赛-赛道二.md' }, - '人才测评': { name: 'AI人才育成L2评审标准', file: 'AI人才育成L2.md' }, + 'L2考核': { name: 'L2考核·评审标准(7维)', file: 'L2考核-评审标准.md' }, }; const TMPL_DIR = path.resolve(__dirname, '../../config/standards'); @@ -127,8 +133,8 @@ function loadDefaultStandard(track: string): { name: string; content: string } | router.post('/', (req: Request, res: Response) => { const { name, description, deadline, track } = req.body; if (!name?.trim()) return res.status(400).json({ error: '项目名称为必填项' }); - const validTracks = ['赛道一', '赛道二', '人才测评']; - if (!validTracks.includes(track)) return res.status(400).json({ error: '赛道为必选项,可选值:赛道一、赛道二、人才测评' }); + const validTracks = ['赛道一', '赛道二', 'L2考核']; + if (!validTracks.includes(track)) return res.status(400).json({ error: '赛道为必选项,可选值:赛道一、赛道二、L2考核' }); const trackVal = track; const id = crypto.randomUUID(); @@ -161,7 +167,7 @@ router.put('/:id', (req: Request, res: Response) => { if (!existing) return; const { name, description, deadline, track } = req.body; - const validTracks = ['赛道一', '赛道二', '人才测评']; + const validTracks = ['赛道一', '赛道二', 'L2考核']; const trackVal = track !== undefined ? (validTracks.includes(track) ? track : '') : (existing as any).track; db.prepare('UPDATE projects SET name = ?, description = ?, deadline = ?, track = ? WHERE id = ?').run( name?.trim() || (existing as any).name, diff --git a/server/src/services/hard-rules.ts b/server/src/services/hard-rules.ts index f36a9c0..de96250 100644 --- a/server/src/services/hard-rules.ts +++ b/server/src/services/hard-rules.ts @@ -47,3 +47,42 @@ export function applyHardRules( }); return { dimensions, log }; } + +// ================= L2考核(7维)硬规则 ================= +// 规则来源:《L2考核成果物提交规范》§11 源码可运行前提 +const L2_FUNC_DIM = '功能完整性'; +const L2_TEST_DIM = '测试用例与测试结果'; +const L2_README_DIM = '代码质量+README'; + +export function applyL2HardRules( + dims: HardRuleDim[], + ctx: HardRuleContext +): HardRuleResult { + const log: string[] = []; + const dimensions = dims.map(d => { + const name = d.name; + let cap = d.maxScore; + // §11:无法按 README 运行属重大缺陷——功能完整性最高10分、测试维度最高5分 + if (ctx.buildFailed && name === L2_FUNC_DIM) cap = Math.min(cap, 10); + if ((ctx.buildFailed || ctx.testStepFailed) && name === L2_TEST_DIM) cap = Math.min(cap, 5); + if (!ctx.hasAnyReadme && name === L2_README_DIM) cap = Math.min(cap, 0); + else if (!ctx.hasRootReadme && name === L2_README_DIM) cap = Math.min(cap, 6); + if (ctx.duplicateRatio > 0.5 && name === L2_README_DIM) cap = Math.min(cap, 3); + let score = d.score; + if (score > cap) { + log.push(`${name} ${score}→${cap}(L2硬规则封顶)`); + score = cap; + } + return { name, score, maxScore: d.maxScore }; + }); + return { dimensions, log }; +} + +/** 按赛道分发:L2考核走 7 维专属规则,其余沿用共通规则 */ +export function applyTrackHardRules( + track: string, + dims: HardRuleDim[], + ctx: HardRuleContext +): HardRuleResult { + return track === 'L2考核' ? applyL2HardRules(dims, ctx) : applyHardRules(dims, ctx); +} diff --git a/server/src/services/l2-topics.ts b/server/src/services/l2-topics.ts new file mode 100644 index 0000000..ce6287e --- /dev/null +++ b/server/src/services/l2-topics.ts @@ -0,0 +1,95 @@ +import fs from 'fs'; +import path from 'path'; + +export interface L2Topic { + id: string; + title: string; + group?: string; + difficulty: number; + maxScoreCap: number; + minTests?: number; + funcBreakdown: string; + acceptance: string[]; + sampleData?: string; +} + +interface L2TopicsFile { + version: string; + note: string; + selfTopic: L2Topic; + topics: L2Topic[]; +} + +const FILE = path.resolve(__dirname, '../../config/l2-topics.json'); +let cache: L2TopicsFile | null = null; + +function load(): L2TopicsFile { + if (cache) return cache; + try { + cache = JSON.parse(fs.readFileSync(FILE, 'utf-8')) as L2TopicsFile; + } catch { + cache = { version: '', note: '', selfTopic: emptySelf(), topics: [] }; + } + return cache; +} + +function emptySelf(): L2Topic { + return { id: 'self', title: '自选题', difficulty: 0, maxScoreCap: 100, funcBreakdown: '', acceptance: [] }; +} + +/** 全部选题(含自选题) */ +export function getL2Topics(): L2TopicsFile { + return load(); +} + +/** 按编号查选题('self' = 自选题);未命中返回 null */ +export function findL2Topic(id: string): L2Topic | null { + if (!id) return null; + const data = load(); + if (id === 'self') return data.selfTopic; + return data.topics.find(t => t.id === id) || null; +} + +/** + * 组装「功能完整性」维度子 Agent 的选题验收基准上下文(extraContext 注入)。 + * 非命题/无匹配时返回空串。 + */ +export function buildL2FuncContext(topicId: string, selfRegistration?: string): string { + const t = findL2Topic(topicId); + if (!t || !topicId) return ''; + const lines: string[] = [ + '', `## 选题与验收基准(评审的功能完整性30分以此为准)`, + `选题:${t.id === 'self' ? '自选题' : `命题 ${t.id}|${t.title}`}(难度${'★'.repeat(t.difficulty)},满分上限${t.maxScoreCap})`, + `功能完整性拆分口径:${t.funcBreakdown}`, + '', + '验收要点:', + ...t.acceptance.map(a => `- ${a}`), + ]; + if (t.minTests) lines.push(`最少测试用例数:${t.minTests} 个`); + if (t.sampleData) lines.push(`题目专属样本数据要求:${t.sampleData}`); + if (t.id === 'self') { + const reg = parseRegistration(selfRegistration); + lines.push(`自选题登记编号:${reg.no || '(未登记)'}`); + if (reg.features) { + lines.push('登记功能清单快照(README 声明低于该范围核心功能的缩减部分按未实现计):'); + for (const f of reg.features) lines.push(`- ${f}`); + } else { + lines.push('登记功能清单:未提供,按 README 声明功能清单核对'); + } + } + return '\n' + lines.join('\n'); +} + +/** 解析自选题登记字段(JSON 字符串 {no, features}),容错 */ +export function parseRegistration(raw?: string | null): { no: string; features: string[] } { + if (!raw) return { no: '', features: [] }; + try { + const obj = typeof raw === 'string' ? JSON.parse(raw) : raw; + const features = Array.isArray(obj?.features) + ? obj.features.map((f: any) => String(f).trim()).filter(Boolean) + : String(obj?.features || '').split(/[\n;;]/).map(s => s.trim()).filter(Boolean); + return { no: String(obj?.no || ''), features }; + } catch { + return { no: '', features: [] }; + } +} diff --git a/server/src/services/pdf.service.ts b/server/src/services/pdf.service.ts index 0b77385..2b88501 100644 --- a/server/src/services/pdf.service.ts +++ b/server/src/services/pdf.service.ts @@ -102,6 +102,29 @@ export function buildEntryHtml(entry: any, project: any, report: any, dims: any[ ${escapeHtml(d.comment || '-')}${d.verifiability?.note ? `
${escapeHtml(d.verifiability.note)}
` : ''} `).join(''); + // L2考核合格判定行(预计算,避免模板内多层嵌套) + const isL2 = project?.track === 'L2考核'; + const l2ResultLine = isL2 && entry.final_level + ? (entry.final_level === '合格' + ? '
L2考核判定:✅ 合格' + : '
L2考核判定:❌ 不合格') + + (entry.late_days > 7 ? '(迟交超过7个工作日,按0分处理)' : '') + : ''; + + // L2考核查重初筛(确定性 flags,仅告警,需人工复核) + let plagBlock = ''; + if (isL2 && entry.plagiarism_json) { + try { + const p = JSON.parse(entry.plagiarism_json); + if (p?.flags?.length) { + plagBlock = '
' + + '查重初筛告警(需人工复核,不构成违规认定):
    ' + + p.flags.map((f: any) => `
  • ${escapeHtml(f.detail)}
  • `).join('') + + '
'; + } + } catch { } + } + return `