check-self-study.mjs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import assert from 'node:assert/strict';
  2. import { readFileSync } from 'node:fs';
  3. import { resolve } from 'node:path';
  4. const modules = [
  5. 'api',
  6. 'database',
  7. 'api-database',
  8. 'tasks',
  9. 'redis',
  10. 'docker',
  11. 'linux',
  12. 'troubleshooting',
  13. 'distributed',
  14. ];
  15. let lessonsChecked = 0;
  16. let stepsChecked = 0;
  17. for (const slug of modules) {
  18. const lessons = JSON.parse(
  19. readFileSync(
  20. resolve(
  21. slug === 'api' ? 'app/lessons.json' : `public/content/${slug}.json`,
  22. ),
  23. 'utf8',
  24. ).replace(/^\uFEFF/, ''),
  25. );
  26. const markdown = readFileSync(
  27. resolve(`public/downloads/${slug}-lessons.md`),
  28. 'utf8',
  29. );
  30. for (const [index, lesson] of lessons.entries()) {
  31. const label = `${slug}/${lesson.id}`;
  32. const marker = `## ${lesson.id} ${lesson.title}\n`;
  33. const start = markdown.indexOf(marker);
  34. assert(start >= 0, `${label}: missing downloaded lesson`);
  35. const next = lessons[index + 1];
  36. const end = next
  37. ? markdown.indexOf(`## ${next.id} ${next.title}\n`, start + marker.length)
  38. : markdown.length;
  39. assert(end > start, `${label}: lesson order changed`);
  40. const lessonText = markdown.slice(start, end);
  41. assert(
  42. !lessonText.includes('### 必要概念'),
  43. `${label}: concepts still front-loaded`,
  44. );
  45. const theory = lesson.kind === 'theory';
  46. const positions = [
  47. '### 从一个问题开始',
  48. '### 这节课要完成什么',
  49. `### ${theory ? '一步步推演场景' : '一步步完成案例'}`,
  50. `### ${theory ? '检查推演结果' : '检查学习结果'}`,
  51. `### ${theory ? '自己设计一次' : '自己试一次'}`,
  52. '### 核心概念回顾',
  53. `### ${theory ? '延伸阅读' : '技术文档'}`,
  54. ].map((heading) => {
  55. const position = lessonText.indexOf(heading);
  56. assert(position >= 0, `${label}: missing section ${heading}`);
  57. return position;
  58. });
  59. assert(
  60. positions.every((position, i) => !i || position > positions[i - 1]),
  61. `${label}: self-study reading order is incorrect`,
  62. );
  63. for (const content of [
  64. lesson.intro.scenario,
  65. lesson.intro.outcome,
  66. ...lesson.intro.startingPoint,
  67. ]) {
  68. assert(
  69. lessonText.slice(0, positions[2]).includes(content),
  70. `${label}: preparation missing from download`,
  71. );
  72. }
  73. let cursor = positions[2];
  74. for (const [stepIndex, step] of lesson.steps.entries()) {
  75. const heading = `#### ${stepIndex + 1}. ${step.title}\n`;
  76. const at = lessonText.indexOf(heading, cursor);
  77. assert(
  78. at >= cursor && at < positions[3],
  79. `${label}: step ${stepIndex + 1} missing or misplaced`,
  80. );
  81. const nextStep = lesson.steps[stepIndex + 1];
  82. const until = nextStep
  83. ? lessonText.indexOf(
  84. `#### ${stepIndex + 2}. ${nextStep.title}\n`,
  85. at + heading.length,
  86. )
  87. : positions[3];
  88. assert(until > at, `${label}: step boundaries are incorrect`);
  89. const stepText = lessonText.slice(at, until);
  90. const actionAt = stepText.indexOf(step.body);
  91. const expectedAt = stepText.indexOf(
  92. `**${theory ? '对照你的推演' : '你应该看到什么'}**`,
  93. );
  94. const explainAt = stepText.indexOf(
  95. `**${theory ? '从这里理解原因' : '刚才发生了什么'}**`,
  96. );
  97. assert(
  98. actionAt >= 0 && expectedAt > actionAt && explainAt > expectedAt,
  99. `${label}/step-${stepIndex + 1}: action → observation → explanation broken`,
  100. );
  101. assert(
  102. stepText.slice(expectedAt, explainAt).includes(step.expected),
  103. `${label}: expected result lost`,
  104. );
  105. assert(
  106. stepText.slice(explainAt).includes(step.explanation),
  107. `${label}: explanation lost`,
  108. );
  109. if (step.code) {
  110. const codeAt = stepText.indexOf(step.code, actionAt + step.body.length);
  111. assert(
  112. codeAt > actionAt && codeAt < expectedAt,
  113. `${label}: code must precede observation`,
  114. );
  115. }
  116. cursor = until;
  117. stepsChecked++;
  118. }
  119. for (const concept of lesson.concepts) {
  120. assert(
  121. lessonText.slice(positions[5], positions[6]).includes(concept.body),
  122. `${label}: concept recap lost`,
  123. );
  124. }
  125. lessonsChecked++;
  126. }
  127. }
  128. console.log(
  129. `Self-study downloads: ${lessonsChecked} lessons and ${stepsChecked} steps preserve preparation, action, observation, explanation and final recap.`,
  130. );