review-content.mjs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. export const reviewModules = [
  2. 'api',
  3. 'database',
  4. 'api-database',
  5. 'tasks',
  6. 'redis',
  7. 'docker',
  8. 'linux',
  9. 'troubleshooting',
  10. 'distributed',
  11. ];
  12. const nonempty = (value) =>
  13. typeof value === 'string' && value.trim().length > 0;
  14. const strings = (value, min) =>
  15. Array.isArray(value) && value.length >= min && value.every(nonempty);
  16. export function verifyReview(lesson, index, sources) {
  17. const errors = [];
  18. const at = `review/${lesson?.id || index + 1}`;
  19. const fail = (message) => errors.push(`${at}: ${message}`);
  20. if (
  21. lesson.kind !== 'review' ||
  22. lesson.id !== String(index + 1).padStart(2, '0') ||
  23. lesson.sourceModule !== reviewModules[index]
  24. )
  25. fail('invalid review identity or source module');
  26. for (const key of ['title', 'summary', 'group'])
  27. if (!nonempty(lesson[key])) fail(`missing ${key}`);
  28. if (
  29. !nonempty(lesson.intro?.scenario) ||
  30. !nonempty(lesson.intro?.outcome) ||
  31. !strings(lesson.intro?.startingPoint, 2)
  32. )
  33. fail('missing self-test preparation');
  34. for (const key of [
  35. 'steps',
  36. 'production',
  37. 'interview',
  38. 'demo',
  39. 'code',
  40. 'concepts',
  41. ])
  42. if (key in lesson)
  43. fail(`review must not contain teaching-only field ${key}`);
  44. const source = sources.get(lesson.sourceModule);
  45. if (!source) fail('missing source lessons');
  46. for (const [key, count, prefix] of [
  47. ['questions', 6, 'q'],
  48. ['challenges', 2, 'c'],
  49. ]) {
  50. if (!Array.isArray(lesson[key]) || lesson[key].length !== count) {
  51. fail(`expected ${count} ${key}`);
  52. continue;
  53. }
  54. for (const [i, item] of lesson[key].entries()) {
  55. if (item.id !== `${prefix}${i + 1}`) fail(`invalid ${key} ID`);
  56. if (
  57. !nonempty(item.title) ||
  58. !nonempty(item.prompt) ||
  59. !strings(item.checkpoints, 3) ||
  60. !nonempty(item.misconception)
  61. )
  62. fail(`incomplete ${item.id}`);
  63. if (
  64. !Array.isArray(item.revisit) ||
  65. item.revisit.length < 1 ||
  66. item.revisit.length > 2
  67. ) {
  68. fail(`${item.id} needs one or two return links`);
  69. continue;
  70. }
  71. const seen = new Set();
  72. for (const link of item.revisit) {
  73. const original = source?.find((entry) => entry.id === link.lessonId);
  74. if (
  75. !original ||
  76. original.title !== link.label ||
  77. seen.has(link.lessonId)
  78. )
  79. fail(`${item.id}: invalid or duplicate return link ${link.lessonId}`);
  80. seen.add(link.lessonId);
  81. }
  82. }
  83. }
  84. if (!Array.isArray(lesson.mastery) || lesson.mastery.length !== 4)
  85. fail('expected four mastery criteria');
  86. else
  87. lesson.mastery.forEach((item, i) => {
  88. if (
  89. item.id !== `m${i + 1}` ||
  90. !nonempty(item.text) ||
  91. !nonempty(item.evidence) ||
  92. item.text === item.evidence
  93. )
  94. fail(`incomplete mastery evidence ${item.id}`);
  95. });
  96. return errors;
  97. }
  98. const bullets = (items) => items.map((item) => `- ${item}`).join('\n');
  99. function links(lesson, item) {
  100. return item.revisit
  101. .map((link) => {
  102. const route =
  103. lesson.sourceModule === 'api'
  104. ? `lesson=${link.lessonId}`
  105. : `module=${lesson.sourceModule}&lesson=${link.lessonId}`;
  106. return `[${link.lessonId} · ${link.label}](../?${route})`;
  107. })
  108. .join(';');
  109. }
  110. export function reviewMarkdown(lessons) {
  111. let result =
  112. '# 复盘与自测\n\n术核 AI · 后端工程课件\n\n每个单元对应一个已学模块。先独立回答,再看文末参考要点;合上答案后仍能解释原因、推演条件变化,并拿出四项掌握证据,才达到本轮标准。记录反映学员自评,不自动判定文字答案的正确性。\n\n';
  113. for (const lesson of lessons) {
  114. 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`;
  115. for (const [title, items] of [
  116. ['核心理解问题', lesson.questions],
  117. ['情境推演', lesson.challenges],
  118. ]) {
  119. result += `### ${title}\n\n先写下判断、原因与验证方式,再对照本单元末尾的参考要点。\n\n`;
  120. for (const item of items)
  121. result += `#### ${item.id.toUpperCase()} ${item.title}\n\n${item.prompt}\n\n我的回答:\n\n自评:□ 还需要回学 □ 合上要点后能独立回答并举例或推演\n\n需要时回学:${links(lesson, item)}\n\n`;
  122. }
  123. result += '### 掌握标准与证据\n\n';
  124. for (const item of lesson.mastery)
  125. result += `- [ ] ${item.text}\n\n 证据:${item.evidence}\n\n`;
  126. result +=
  127. '### 本轮结果与回学计划\n\n八项问题均能独立回答,且四项证据均已核对,才达到本轮标准。未完成的题按回学链接补课,再闭卷重答;已经完成的题,隔一天换个条件再次推演。\n\n需要回学的题号:\n\n尚缺的证据:\n\n下次复测时准备改变的条件:\n\n### 参考要点与易错点\n\n完成自己的回答后再阅读。表述可以不同,要核对的是推理、边界和验证依据。\n\n';
  128. for (const item of [...lesson.questions, ...lesson.challenges])
  129. result += `#### ${item.id.toUpperCase()} ${item.title}\n\n${bullets(item.checkpoints)}\n\n容易误判:${item.misconception}\n\n`;
  130. result += '---\n\n';
  131. }
  132. return result;
  133. }