/** * 根据 config/lab-task-description.md 与仓库扫描结果生成「实训步骤 / 评测对齐」表(服务端权威,不依赖大模型编造)。 * 可选合并 chat_logs 启发式结果中的 s3/s5。 */ function heuristicStepDone(heuristicReport, id) { if (!heuristicReport?.summary?.rubric_steps) return false; const row = heuristicReport.summary.rubric_steps.find((s) => s.id === id); return Boolean(row?.done); } function pySnippet(scan) { return scan?.score_analysis_py?.snippet || ''; } /** * @param {string} taskMd * @param {object} scan scanStudentRepo 返回值 * @param {number} fileCount * @param {string} gitUrl * @param {object|null} heuristicReport analyzeChatLogsFile 完整结果或 null */ function buildRubricFromTaskAndScan(taskMd, scan, fileCount, gitUrl, heuristicReport) { scan = scan || {}; const taskBrief = (taskMd || '').trim().slice(0, 200).replace(/\s+/g, ' '); const py = pySnippet(scan); const hasPy = Boolean(scan?.score_analysis_py); const hasCsv = Boolean(scan?.scores_csv); const pngOk = Boolean(scan?.score_chart_png?.exists); const f = scan?.flags || {}; const rows = []; rows.push({ id: 'prep', label: '准备数据 scores.csv', done: hasCsv, eval_note: hasCsv ? `任务要求准备成绩表;仓库中已找到 ${scan.scores_csv.path}(${scan.scores_csv.bytes} 字节)。${taskBrief ? `任务描述摘要:${taskBrief}` : ''}` : `任务要求准备 scores.csv 等成绩数据;当前仓库扫描未匹配到典型 CSV(请确认是否放在子目录或文件名不同)。`, }); rows.push({ id: 's1', label: '步骤一:打开 Cursor / 新建项目', done: fileCount > 0, eval_note: fileCount > 0 ? `已从远程仓库拉取共 ${fileCount} 个文件,说明学员工作区非空,可认为具备开展实训的工程基础。` : '仓库内未发现文件,无法验证本地 IDE 操作步骤。', }); const s2Done = hasPy && (f.has_read_csv || f.has_mean); rows.push({ id: 's2', label: '步骤二:自然语言生成成绩统计代码', done: s2Done, eval_note: hasPy ? `已发现主脚本 ${scan.score_analysis_py.path}。${f.has_read_csv ? '含 read_csv/pandas 读取迹象。' : '未从片段中检出 read_csv。'}${f.has_mean ? '含 mean/统计迹象。' : '未从片段中检出 mean。'}` : '未在仓库中定位 score_analysis.py 或同类分析脚本,无法对齐头歌「脚本含 read_csv、mean」评测项。', }); const s3FromChat = heuristicStepDone(heuristicReport, 's3'); const s3FromPy = /traceback|exception|raise |except |error|报错/i.test(py); rows.push({ id: 's3', label: '步骤三:运行与调试', done: s3FromChat || s3FromPy, eval_note: s3FromChat ? '对话日志侧曾出现与排错/修复相关的关键词,记为已体现调试环节。' : s3FromPy ? '脚本片段中出现异常处理或错误相关关键字,可能经历过排错(弱推断)。' : '仓库脚本片段与对话均未明确体现「运行—报错—修复」闭环,建议结合终端截图补充。', }); const pngPath = scan.score_chart_png?.path || 'score_chart.png'; rows.push({ id: 's4', label: '步骤四:matplotlib 柱状图与 score_chart.png', done: pngOk && f.has_matplotlib, eval_note: pngOk ? `已发现图表文件 ${pngPath}(${scan.score_chart_png.bytes} 字节)。${f.has_matplotlib ? '脚本中含 matplotlib/pyplot 迹象。' : '脚本片段中未检出 matplotlib,请人工核对图表是否由其他方式生成。'}` : `未检出 score_chart.png。任务要求保存柱状图;请对照头歌评测「score_chart.png 存在」。${f.has_matplotlib ? '脚本中已有作图代码,可能尚未运行保存。' : ''}`, }); const s5FromChat = heuristicStepDone(heuristicReport, 's5'); rows.push({ id: 's5', label: '步骤五:请 AI 解释代码', done: s5FromChat, eval_note: s5FromChat ? '对话日志侧出现「解释」等与代码理解相关的提问,记为已体现。' : '仓库无法直接证明「请 AI 解释」步骤;若学员未导出相关对话,此项可能为否。', }); rows.push({ id: 's6', label: '步骤六:保存 score_analysis.py', done: hasPy, eval_note: hasPy ? `仓库中存在 ${scan.score_analysis_py.path},满足「提交主脚本」类评测的文件存在性检查。` : '未找到 score_analysis.py,与头歌预期交付物不一致。', }); return rows; } module.exports = { buildRubricFromTaskAndScan };