check-course-diagrams.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import assert from 'node:assert/strict';
  2. import { readFile } from 'node:fs/promises';
  3. import { fileURLToPath } from 'node:url';
  4. import path from 'node:path';
  5. import {
  6. teachingDiagrams,
  7. getModuleDiagrams,
  8. hasLessonDiagrams,
  9. } from '../app/diagram-catalog.ts';
  10. const root = fileURLToPath(new URL('../', import.meta.url));
  11. const modules = [
  12. 'api',
  13. 'database',
  14. 'api-database',
  15. 'tasks',
  16. 'redis',
  17. 'docker',
  18. 'linux',
  19. 'troubleshooting',
  20. 'distributed',
  21. ];
  22. const lessons = new Map();
  23. for (const moduleSlug of modules) {
  24. const file =
  25. moduleSlug === 'api'
  26. ? 'app/lessons.json'
  27. : `public/content/${moduleSlug}.json`;
  28. const content = JSON.parse(await readFile(path.join(root, file), 'utf8'));
  29. lessons.set(moduleSlug, new Set(content.map((lesson) => lesson.id)));
  30. }
  31. const diagramIds = new Set();
  32. for (const diagram of teachingDiagrams) {
  33. assert(!diagramIds.has(diagram.id), `Duplicate diagram: ${diagram.id}`);
  34. diagramIds.add(diagram.id);
  35. assert(
  36. lessons.has(diagram.module),
  37. `${diagram.id}: unknown module ${diagram.module}`,
  38. );
  39. assert(diagram.lessons.length > 0, `${diagram.id}: missing lesson mapping`);
  40. assert.equal(
  41. new Set(diagram.lessons).size,
  42. diagram.lessons.length,
  43. `${diagram.id}: duplicate lesson mapping`,
  44. );
  45. for (const lesson of diagram.lessons) {
  46. assert(
  47. lessons.get(diagram.module).has(lesson),
  48. `${diagram.id}: unknown lesson ${diagram.module}/${lesson}`,
  49. );
  50. }
  51. const nodeIds = new Set(diagram.nodes.map((node) => node.id));
  52. assert.equal(
  53. nodeIds.size,
  54. diagram.nodes.length,
  55. `${diagram.id}: duplicate node ID`,
  56. );
  57. assert(
  58. diagram.steps.length >= 3 && diagram.misconception.length > 0,
  59. `${diagram.id}: missing text explanation`,
  60. );
  61. for (const node of diagram.nodes) {
  62. assert(
  63. node.x >= 0 && node.x + 210 <= 900,
  64. `${diagram.id}/${node.id}: node outside SVG width`,
  65. );
  66. assert(
  67. node.y >= 0 && node.y + 104 <= diagram.rows.length * 185 + 60,
  68. `${diagram.id}/${node.id}: node outside SVG height`,
  69. );
  70. const lineCount = (text, maxUnits) => {
  71. let lines = 1;
  72. let units = 0;
  73. for (const character of text) {
  74. const width = /[\u0020-\u007e]/.test(character) ? 1 : 2;
  75. if (units + width > maxUnits) {
  76. lines++;
  77. units = 0;
  78. }
  79. units += width;
  80. }
  81. return lines;
  82. };
  83. const bottomBaseline =
  84. 26 +
  85. lineCount(node.title, 24) * 19 +
  86. 5 +
  87. (lineCount(node.detail, 29) - 1) * 16;
  88. assert(
  89. bottomBaseline <= 98,
  90. `${diagram.id}/${node.id}: text exceeds node height`,
  91. );
  92. }
  93. for (const edge of diagram.edges) {
  94. assert(
  95. nodeIds.has(edge.from) && nodeIds.has(edge.to),
  96. `${diagram.id}: dangling edge ${edge.from} -> ${edge.to}`,
  97. );
  98. for (const [x, y] of [
  99. ...(edge.via ?? []),
  100. ...(edge.labelAt ? [edge.labelAt] : []),
  101. ]) {
  102. assert(
  103. x >= 0 && x <= 900 && y >= 0 && y <= diagram.rows.length * 185 + 60,
  104. `${diagram.id}: edge outside viewBox`,
  105. );
  106. }
  107. }
  108. }
  109. for (const moduleSlug of modules) {
  110. assert(
  111. getModuleDiagrams(moduleSlug).length >= 1,
  112. `${moduleSlug}: missing overview diagram`,
  113. );
  114. }
  115. for (const lesson of lessons.get('distributed')) {
  116. assert(
  117. hasLessonDiagrams('distributed', lesson),
  118. `distributed/${lesson}: missing diagram`,
  119. );
  120. }
  121. console.log(
  122. `Validated ${teachingDiagrams.length} diagrams: unique IDs, valid edges, text bounds, lesson mappings, 9 module overviews, and all distributed lessons.`,
  123. );
  124. for (const moduleSlug of modules) {
  125. const covered = [...lessons.get(moduleSlug)].filter((lesson) =>
  126. hasLessonDiagrams(moduleSlug, lesson),
  127. ).length;
  128. console.log(
  129. `${moduleSlug}: ${teachingDiagrams.filter((diagram) => diagram.module === moduleSlug).length} diagrams, ${covered}/${lessons.get(moduleSlug).size} lessons illustrated`,
  130. );
  131. }