export const reviewModules = [ 'api', 'database', 'api-database', 'tasks', 'redis', 'docker', 'linux', 'troubleshooting', 'distributed', ]; const nonempty = (value) => typeof value === 'string' && value.trim().length > 0; const strings = (value, min) => Array.isArray(value) && value.length >= min && value.every(nonempty); export function verifyReview(lesson, index, sources) { const errors = []; const at = `review/${lesson?.id || index + 1}`; const fail = (message) => errors.push(`${at}: ${message}`); if ( lesson.kind !== 'review' || lesson.id !== String(index + 1).padStart(2, '0') || lesson.sourceModule !== reviewModules[index] ) fail('invalid review identity or source module'); for (const key of ['title', 'summary', 'group']) if (!nonempty(lesson[key])) fail(`missing ${key}`); if ( !nonempty(lesson.intro?.scenario) || !nonempty(lesson.intro?.outcome) || !strings(lesson.intro?.startingPoint, 2) ) fail('missing self-test preparation'); for (const key of [ 'steps', 'production', 'interview', 'demo', 'code', 'concepts', ]) if (key in lesson) fail(`review must not contain teaching-only field ${key}`); const source = sources.get(lesson.sourceModule); if (!source) fail('missing source lessons'); for (const [key, count, prefix] of [ ['questions', 6, 'q'], ['challenges', 2, 'c'], ]) { if (!Array.isArray(lesson[key]) || lesson[key].length !== count) { fail(`expected ${count} ${key}`); continue; } for (const [i, item] of lesson[key].entries()) { if (item.id !== `${prefix}${i + 1}`) fail(`invalid ${key} ID`); if ( !nonempty(item.title) || !nonempty(item.prompt) || !strings(item.checkpoints, 3) || !nonempty(item.misconception) ) fail(`incomplete ${item.id}`); if ( !Array.isArray(item.revisit) || item.revisit.length < 1 || item.revisit.length > 2 ) { fail(`${item.id} needs one or two return links`); continue; } const seen = new Set(); for (const link of item.revisit) { const original = source?.find((entry) => entry.id === link.lessonId); if ( !original || original.title !== link.label || seen.has(link.lessonId) ) fail(`${item.id}: invalid or duplicate return link ${link.lessonId}`); seen.add(link.lessonId); } } } if (!Array.isArray(lesson.mastery) || lesson.mastery.length !== 4) fail('expected four mastery criteria'); else lesson.mastery.forEach((item, i) => { if ( item.id !== `m${i + 1}` || !nonempty(item.text) || !nonempty(item.evidence) || item.text === item.evidence ) fail(`incomplete mastery evidence ${item.id}`); }); return errors; } const bullets = (items) => items.map((item) => `- ${item}`).join('\n'); function links(lesson, item) { return item.revisit .map((link) => { const route = lesson.sourceModule === 'api' ? `lesson=${link.lessonId}` : `module=${lesson.sourceModule}&lesson=${link.lessonId}`; return `[${link.lessonId} · ${link.label}](../?${route})`; }) .join(';'); } export function reviewMarkdown(lessons) { let result = '# 复盘与自测\n\n术核 AI · 后端工程课件\n\n每个单元对应一个已学模块。先独立回答,再看文末参考要点;合上答案后仍能解释原因、推演条件变化,并拿出四项掌握证据,才达到本轮标准。记录反映学员自评,不自动判定文字答案的正确性。\n\n'; for (const lesson of lessons) { result += `## ${lesson.id} ${lesson.title}\n\n${lesson.summary}\n\n### 如何自测\n\n${lesson.intro.scenario}\n\n${bullets(lesson.intro.startingPoint)}\n\n本轮目标:${lesson.intro.outcome}\n\n原模块讲义:[${lesson.sourceModule}-lessons.md](./${lesson.sourceModule}-lessons.md)。离线阅读时按题目下的课号查找。\n\n`; for (const [title, items] of [ ['核心理解问题', lesson.questions], ['情境推演', lesson.challenges], ]) { result += `### ${title}\n\n先写下判断、原因与验证方式,再对照本单元末尾的参考要点。\n\n`; for (const item of items) result += `#### ${item.id.toUpperCase()} ${item.title}\n\n${item.prompt}\n\n我的回答:\n\n自评:□ 还需要回学 □ 合上要点后能独立回答并举例或推演\n\n需要时回学:${links(lesson, item)}\n\n`; } result += '### 掌握标准与证据\n\n'; for (const item of lesson.mastery) result += `- [ ] ${item.text}\n\n 证据:${item.evidence}\n\n`; result += '### 本轮结果与回学计划\n\n八项问题均能独立回答,且四项证据均已核对,才达到本轮标准。未完成的题按回学链接补课,再闭卷重答;已经完成的题,隔一天换个条件再次推演。\n\n需要回学的题号:\n\n尚缺的证据:\n\n下次复测时准备改变的条件:\n\n### 参考要点与易错点\n\n完成自己的回答后再阅读。表述可以不同,要核对的是推理、边界和验证依据。\n\n'; for (const item of [...lesson.questions, ...lesson.challenges]) result += `#### ${item.id.toUpperCase()} ${item.title}\n\n${bullets(item.checkpoints)}\n\n容易误判:${item.misconception}\n\n`; result += '---\n\n'; } return result; }