import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; const modules = [ 'api', 'database', 'api-database', 'tasks', 'redis', 'docker', 'linux', 'troubleshooting', 'distributed', ]; let lessonsChecked = 0; let stepsChecked = 0; for (const slug of modules) { const lessons = JSON.parse( readFileSync( resolve( slug === 'api' ? 'app/lessons.json' : `public/content/${slug}.json`, ), 'utf8', ).replace(/^\uFEFF/, ''), ); const markdown = readFileSync( resolve(`public/downloads/${slug}-lessons.md`), 'utf8', ); for (const [index, lesson] of lessons.entries()) { const label = `${slug}/${lesson.id}`; const marker = `## ${lesson.id} ${lesson.title}\n`; const start = markdown.indexOf(marker); assert(start >= 0, `${label}: missing downloaded lesson`); const next = lessons[index + 1]; const end = next ? markdown.indexOf(`## ${next.id} ${next.title}\n`, start + marker.length) : markdown.length; assert(end > start, `${label}: lesson order changed`); const lessonText = markdown.slice(start, end); assert( !lessonText.includes('### 必要概念'), `${label}: concepts still front-loaded`, ); const theory = lesson.kind === 'theory'; const positions = [ '### 从一个问题开始', '### 这节课要完成什么', `### ${theory ? '一步步推演场景' : '一步步完成案例'}`, `### ${theory ? '检查推演结果' : '检查学习结果'}`, `### ${theory ? '自己设计一次' : '自己试一次'}`, '### 核心概念回顾', `### ${theory ? '延伸阅读' : '技术文档'}`, ].map((heading) => { const position = lessonText.indexOf(heading); assert(position >= 0, `${label}: missing section ${heading}`); return position; }); assert( positions.every((position, i) => !i || position > positions[i - 1]), `${label}: self-study reading order is incorrect`, ); for (const content of [ lesson.intro.scenario, lesson.intro.outcome, ...lesson.intro.startingPoint, ]) { assert( lessonText.slice(0, positions[2]).includes(content), `${label}: preparation missing from download`, ); } let cursor = positions[2]; for (const [stepIndex, step] of lesson.steps.entries()) { const heading = `#### ${stepIndex + 1}. ${step.title}\n`; const at = lessonText.indexOf(heading, cursor); assert( at >= cursor && at < positions[3], `${label}: step ${stepIndex + 1} missing or misplaced`, ); const nextStep = lesson.steps[stepIndex + 1]; const until = nextStep ? lessonText.indexOf( `#### ${stepIndex + 2}. ${nextStep.title}\n`, at + heading.length, ) : positions[3]; assert(until > at, `${label}: step boundaries are incorrect`); const stepText = lessonText.slice(at, until); const actionAt = stepText.indexOf(step.body); const expectedAt = stepText.indexOf( `**${theory ? '对照你的推演' : '你应该看到什么'}**`, ); const explainAt = stepText.indexOf( `**${theory ? '从这里理解原因' : '刚才发生了什么'}**`, ); assert( actionAt >= 0 && expectedAt > actionAt && explainAt > expectedAt, `${label}/step-${stepIndex + 1}: action → observation → explanation broken`, ); assert( stepText.slice(expectedAt, explainAt).includes(step.expected), `${label}: expected result lost`, ); assert( stepText.slice(explainAt).includes(step.explanation), `${label}: explanation lost`, ); if (step.code) { const codeAt = stepText.indexOf(step.code, actionAt + step.body.length); assert( codeAt > actionAt && codeAt < expectedAt, `${label}: code must precede observation`, ); } cursor = until; stepsChecked++; } for (const concept of lesson.concepts) { assert( lessonText.slice(positions[5], positions[6]).includes(concept.body), `${label}: concept recap lost`, ); } lessonsChecked++; } } console.log( `Self-study downloads: ${lessonsChecked} lessons and ${stepsChecked} steps preserve preparation, action, observation, explanation and final recap.`, );