|
|
/**
|
|
|
* 根据 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 };
|