coursecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,517 @@
1
+ /**
2
+ * Shared Validation Rules
3
+ *
4
+ * Pure validation logic used by both:
5
+ * - framework/js/dev/runtime-linter.js (browser, uses DOM)
6
+ * - lib/build-linter.js (Node.js, uses source-parser)
7
+ *
8
+ * These functions contain no environment-specific code (no DOM, no fs).
9
+ * They operate on plain JavaScript objects.
10
+ */
11
+
12
+ /**
13
+ * Flattens a hierarchical structure into a flat array of slides.
14
+ * @param {array} structure - The structure array (may contain sections with children)
15
+ * @returns {array} Flat array of slide objects
16
+ */
17
+ export function flattenStructure(structure) {
18
+ const slides = [];
19
+
20
+ function traverse(items) {
21
+ for (const item of items) {
22
+ if (item.children) {
23
+ traverse(item.children);
24
+ } else if (item.component) {
25
+ slides.push(item);
26
+ }
27
+ }
28
+ }
29
+
30
+ traverse(structure);
31
+ return slides;
32
+ }
33
+
34
+ /**
35
+ * Registers an interaction ID and checks for duplicates.
36
+ * @param {string} id - The interaction ID to register
37
+ * @param {string} sourceName - The name of the source (e.g., slide ID)
38
+ * @param {string} sourceType - The type of interaction (e.g., 'DOM', 'Assessment')
39
+ * @param {Map} registry - The registry map
40
+ * @param {array} errors - The errors array to push to if duplicate found
41
+ */
42
+ export function registerInteractionId(id, sourceName, sourceType, registry, errors) {
43
+ if (!id) return;
44
+
45
+ if (registry.has(id)) {
46
+ const existing = registry.get(id);
47
+ errors.push(`Duplicate ID "${id}": Found in ${sourceType} "${sourceName}" but already declared in ${existing.sourceType} "${existing.sourceName}". All interaction, assessment, and question IDs must be unique across the entire course.`);
48
+ } else {
49
+ registry.set(id, { sourceName, sourceType });
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validates global course configuration (objectives, orphans).
55
+ * @param {object} courseConfig - The full course configuration object
56
+ * @param {array} slides - Flattened array of slide objects
57
+ * @param {Set} slideFilesOnDisk - Set of slide file paths on disk (for orphan check)
58
+ * @returns {{ warnings: array, objectiveIds: Set }}
59
+ */
60
+ export function validateGlobalConfig(courseConfig, slides, slideFilesOnDisk = new Set()) {
61
+ const warnings = [];
62
+ const slideComponentPaths = new Set(slides.map(s => s.component));
63
+ const allObjectiveIds = new Set();
64
+ const allSlideIds = new Set(slides.map(s => s.id));
65
+
66
+ // Check for orphaned slide files
67
+ for (const knownFile of slideFilesOnDisk) {
68
+ if (!slideComponentPaths.has(knownFile)) {
69
+ warnings.push(`Orphaned File: Slide module "${knownFile}" exists but is not used in the course structure.`);
70
+ }
71
+ }
72
+
73
+ // Validate objectives
74
+ if (courseConfig.objectives && Array.isArray(courseConfig.objectives)) {
75
+ for (const objective of courseConfig.objectives) {
76
+ if (!objective.id) {
77
+ warnings.push('Objective missing required \'id\' property.');
78
+ continue;
79
+ }
80
+ allObjectiveIds.add(objective.id);
81
+
82
+ if (objective.criteria) {
83
+ const criteria = objective.criteria;
84
+
85
+ if (criteria.type === 'slideVisited' && criteria.slideId && !allSlideIds.has(criteria.slideId)) {
86
+ warnings.push(`Objective "${objective.id}" has 'slideVisited' criteria with an invalid slideId: "${criteria.slideId}".`);
87
+ }
88
+
89
+ if (criteria.type === 'allSlidesVisited' && Array.isArray(criteria.slideIds)) {
90
+ for (const slideId of criteria.slideIds) {
91
+ if (!allSlideIds.has(slideId)) {
92
+ warnings.push(`Objective "${objective.id}" has 'allSlidesVisited' criteria with an invalid slideId: "${slideId}".`);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (criteria.type === 'timeOnSlide' && criteria.slideId && !allSlideIds.has(criteria.slideId)) {
98
+ warnings.push(`Objective "${objective.id}" has 'timeOnSlide' criteria with an invalid slideId: "${criteria.slideId}".`);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return { warnings, objectiveIds: allObjectiveIds };
105
+ }
106
+
107
+ /**
108
+ * Validates assessment configuration.
109
+ * @param {object} assessmentConfig - The assessment configuration object
110
+ * @param {string} slideId - The slide identifier
111
+ * @param {Set} objectiveIds - Valid objective IDs
112
+ * @param {array} errors - Array to collect errors
113
+ * @param {array} warnings - Array to collect warnings
114
+ * @param {Map} interactionIdRegistry - Registry for checking duplicate IDs
115
+ */
116
+ export function validateAssessmentConfig(assessmentConfig, slideId, objectiveIds, errors, warnings, interactionIdRegistry) {
117
+ // Basic structure validation
118
+ if (!assessmentConfig.id) {
119
+ errors.push(`[${slideId}] Assessment missing required 'id' property`);
120
+ } else {
121
+ registerInteractionId(assessmentConfig.id, slideId, 'Assessment', interactionIdRegistry, errors);
122
+ }
123
+
124
+ // Validate assessmentObjective link
125
+ if (assessmentConfig.assessmentObjective) {
126
+ if (!objectiveIds.has(assessmentConfig.assessmentObjective)) {
127
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' has an invalid assessmentObjective: "${assessmentConfig.assessmentObjective}". This objective ID does not exist in the course configuration.`);
128
+ }
129
+ }
130
+
131
+ // Check for runtime-defined questions (skip validation if so)
132
+ const hasRuntimeQuestions = assessmentConfig._hasRuntimeQuestions;
133
+ const hasRuntimeQuestionBanks = assessmentConfig._hasRuntimeQuestionBanks;
134
+
135
+ // Validate question source
136
+ const hasQuestions = Array.isArray(assessmentConfig.questions) && assessmentConfig.questions.length > 0;
137
+ const hasBanks = Array.isArray(assessmentConfig.questionBanks) && assessmentConfig.questionBanks.length > 0;
138
+
139
+ if (!hasRuntimeQuestions && !hasRuntimeQuestionBanks) {
140
+ if (!hasQuestions && !hasBanks) {
141
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' must have either 'questions' or 'questionBanks' array`);
142
+ return;
143
+ }
144
+
145
+ if (hasQuestions && hasBanks) {
146
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' cannot have both 'questions' and 'questionBanks' - use one or the other`);
147
+ }
148
+ }
149
+
150
+ // Validate settings
151
+ const settings = assessmentConfig.settings || {};
152
+
153
+ if (settings.passingScore != null) {
154
+ if (typeof settings.passingScore !== 'number') {
155
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' passingScore must be a number, got ${typeof settings.passingScore}`);
156
+ } else if (settings.passingScore < 0 || settings.passingScore > 100) {
157
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' passingScore must be 0-100, got ${settings.passingScore}`);
158
+ }
159
+ }
160
+
161
+ if (settings.randomizeQuestions !== undefined && typeof settings.randomizeQuestions !== 'boolean') {
162
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' randomizeQuestions must be boolean, got ${typeof settings.randomizeQuestions}`);
163
+ }
164
+
165
+ if (settings.randomizeOnRetake !== undefined && typeof settings.randomizeOnRetake !== 'boolean') {
166
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' randomizeOnRetake must be boolean, got ${typeof settings.randomizeOnRetake}`);
167
+ }
168
+
169
+ // Validate remedial/restart relationship
170
+ if (settings.attemptsBeforeRestart && settings.attemptsBeforeRemedial) {
171
+ if (settings.attemptsBeforeRestart <= settings.attemptsBeforeRemedial) {
172
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' attemptsBeforeRestart (${settings.attemptsBeforeRestart}) must be > attemptsBeforeRemedial (${settings.attemptsBeforeRemedial})`);
173
+ }
174
+ }
175
+
176
+ if (settings.attemptsBeforeRemedial && !settings.remedialSlideIds) {
177
+ errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' has attemptsBeforeRemedial but no remedialSlideIds`);
178
+ }
179
+
180
+ if (settings.remedialSlideIds && settings.remedialSlideIds.length > 0 && !settings.attemptsBeforeRemedial) {
181
+ warnings.push(`[${slideId}] Assessment '${assessmentConfig.id}' has remedialSlideIds but no attemptsBeforeRemedial (slides won't be used)`);
182
+ }
183
+
184
+ // Skip question validation if runtime-defined
185
+ if (hasRuntimeQuestions || hasRuntimeQuestionBanks) {
186
+ return;
187
+ }
188
+
189
+ // Validate questions
190
+ if (hasQuestions) {
191
+ assessmentConfig.questions.forEach((q, idx) => {
192
+ validateQuestionConfig(q, `${slideId} Question ${idx + 1}`, errors, interactionIdRegistry);
193
+ });
194
+ }
195
+
196
+ // Validate question banks
197
+ if (hasBanks) {
198
+ assessmentConfig.questionBanks.forEach((bank, bankIdx) => {
199
+ const bankRef = `${slideId} Bank ${bankIdx + 1}`;
200
+
201
+ if (!bank.id) {
202
+ errors.push(`${bankRef} missing required 'id' property`);
203
+ }
204
+
205
+ if (!Array.isArray(bank.questions) || bank.questions.length === 0) {
206
+ errors.push(`${bankRef} must have at least one question in 'questions' array`);
207
+ }
208
+
209
+ if (bank.selectCount == null) {
210
+ errors.push(`${bankRef} missing required 'selectCount' property`);
211
+ } else if (bank.selectCount !== 'all') {
212
+ if (typeof bank.selectCount !== 'number') {
213
+ errors.push(`${bankRef} selectCount must be number or 'all', got ${typeof bank.selectCount}`);
214
+ } else if (bank.selectCount <= 0) {
215
+ errors.push(`${bankRef} selectCount must be positive, got ${bank.selectCount}`);
216
+ } else if (bank.questions && bank.selectCount > bank.questions.length) {
217
+ errors.push(`${bankRef} selectCount (${bank.selectCount}) exceeds available questions (${bank.questions.length})`);
218
+ }
219
+ }
220
+
221
+ if (bank.questions) {
222
+ bank.questions.forEach((q, qIdx) => {
223
+ validateQuestionConfig(q, `${bankRef} Question ${qIdx + 1}`, errors, interactionIdRegistry);
224
+ });
225
+ }
226
+ });
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Validates a single question configuration.
232
+ * @param {object} question - The question configuration object
233
+ * @param {string} ref - Reference string for error messages
234
+ * @param {array} errors - Array to collect errors
235
+ * @param {Map} interactionIdRegistry - Registry for checking duplicate IDs
236
+ */
237
+ export function validateQuestionConfig(question, ref, errors, interactionIdRegistry) {
238
+ if (!question.type) {
239
+ errors.push(`${ref} missing required 'type' property`);
240
+ }
241
+
242
+ if (!question.prompt && !question.questionText) {
243
+ errors.push(`${ref} missing required 'prompt' property`);
244
+ }
245
+
246
+ if (question.weight == null) {
247
+ errors.push(`${ref} missing required 'weight' property`);
248
+ } else if (typeof question.weight === 'number' && question.weight <= 0) {
249
+ errors.push(`${ref} weight must be positive, got ${question.weight}`);
250
+ }
251
+
252
+ if (!question.id) {
253
+ errors.push(`${ref} missing required 'id' property`);
254
+ } else {
255
+ registerInteractionId(question.id, ref, 'Question', interactionIdRegistry, errors);
256
+ }
257
+
258
+ // Type-specific validation
259
+ if (question.type === 'multiple-choice' || question.type === 'multiple-choice-single') {
260
+ if (!question.correctAnswer && !question.multiple) {
261
+ errors.push(`${ref} (${question.type}) missing required 'correctAnswer' property`);
262
+ }
263
+ }
264
+
265
+ if (question.type && question.type.startsWith('multiple-choice')) {
266
+ if (!Array.isArray(question.choices) || question.choices.length === 0) {
267
+ errors.push(`${ref} (${question.type}) must have at least one choice in 'choices' array`);
268
+ }
269
+ }
270
+
271
+ if (question.type === 'true-false') {
272
+ if (question.correctAnswer !== true && question.correctAnswer !== false) {
273
+ errors.push(`${ref} (true-false) correctAnswer must be boolean (true or false)`);
274
+ }
275
+ }
276
+
277
+ if (question.type === 'numeric') {
278
+ if (question.correctAnswer == null) {
279
+ errors.push(`${ref} (numeric) missing required 'correctAnswer' property`);
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Validates engagement configuration for a slide.
286
+ * @param {object} slide - The slide configuration
287
+ * @param {array} errors - Array to collect errors
288
+ * @param {array} warnings - Array to collect warnings
289
+ */
290
+ export function validateEngagement(slide, errors, warnings) {
291
+ if (!slide.engagement) {
292
+ errors.push(`Slide "${slide.id}" (${slide.component}) is missing required 'engagement' configuration. Add "engagement: { required: false }" at minimum.`);
293
+ return false;
294
+ }
295
+
296
+ const engagement = slide.engagement;
297
+
298
+ if (engagement.required) {
299
+ if (!engagement.requirements || !Array.isArray(engagement.requirements)) {
300
+ errors.push(`Slide "${slide.id}" has engagement.required=true but no requirements array defined.`);
301
+ return false;
302
+ }
303
+
304
+ if (engagement.requirements.length === 0) {
305
+ warnings.push(`Slide "${slide.id}" has engagement.required=true but empty requirements array. Set required=false if no tracking needed.`);
306
+ }
307
+
308
+ if (engagement.mode && !['all', 'any'].includes(engagement.mode)) {
309
+ errors.push(`Slide "${slide.id}" has invalid engagement.mode "${engagement.mode}". Must be "all" or "any".`);
310
+ }
311
+ }
312
+
313
+ return true;
314
+ }
315
+
316
+ /**
317
+ * Validates a requirement configuration (structure only, not content).
318
+ * Content validation (e.g., checking if tabs/accordion exist) is environment-specific.
319
+ *
320
+ * @param {string} slideId - The slide identifier
321
+ * @param {object} requirement - The requirement configuration
322
+ * @param {array} errors - Array to collect errors
323
+ * @param {array} warnings - Array to collect warnings
324
+ * @param {object} engagementTrackingMap - Reverse map: engagementTracking value -> component type
325
+ */
326
+ export function validateRequirementConfig(slideId, requirement, errors, warnings, engagementTrackingMap = {}) {
327
+ const type = requirement.type;
328
+
329
+ // Component-linked requirement types — auto-recognized from schemas.
330
+ // Content validation (does the component exist?) is environment-specific.
331
+ if (engagementTrackingMap[type]) {
332
+ return;
333
+ }
334
+
335
+ // Config-only requirement types — validate required properties
336
+ switch (type) {
337
+ case 'interactionComplete':
338
+ if (!requirement.interactionId) {
339
+ errors.push(`Slide "${slideId}" has 'interactionComplete' requirement without interactionId.`);
340
+ }
341
+ break;
342
+
343
+ case 'allInteractionsComplete':
344
+ break;
345
+
346
+ case 'scrollDepth': {
347
+ const percentage = requirement.percentage || requirement.minPercentage;
348
+ if (!percentage) {
349
+ errors.push(`Slide "${slideId}" has 'scrollDepth' requirement without percentage.`);
350
+ } else if (percentage < 0 || percentage > 100) {
351
+ errors.push(`Slide "${slideId}" scrollDepth percentage must be 0-100 (got ${percentage}).`);
352
+ }
353
+ break;
354
+ }
355
+
356
+ case 'timeOnSlide':
357
+ if (!requirement.seconds || requirement.seconds <= 0) {
358
+ errors.push(`Slide "${slideId}" has 'timeOnSlide' requirement without valid seconds value.`);
359
+ }
360
+ break;
361
+
362
+ case 'videoComplete':
363
+ if (!requirement.videoId) {
364
+ errors.push(`Slide "${slideId}" has 'videoComplete' requirement without videoId.`);
365
+ }
366
+ break;
367
+
368
+ case 'audioComplete':
369
+ if (!requirement.audioId) {
370
+ errors.push(`Slide "${slideId}" has 'audioComplete' requirement without audioId.`);
371
+ }
372
+ break;
373
+
374
+ case 'slideAudioComplete':
375
+ break;
376
+
377
+ case 'modalAudioComplete':
378
+ if (!requirement.modalId) {
379
+ errors.push(`Slide "${slideId}" has 'modalAudioComplete' requirement without modalId.`);
380
+ }
381
+ break;
382
+
383
+ case 'flag':
384
+ if (!requirement.key) {
385
+ errors.push(`Slide "${slideId}" has 'flag' requirement without key property.`);
386
+ }
387
+ break;
388
+
389
+ case 'allFlags':
390
+ if (!requirement.flags || !Array.isArray(requirement.flags)) {
391
+ errors.push(`Slide "${slideId}" has 'allFlags' requirement without flags array.`);
392
+ } else if (requirement.flags.length === 0) {
393
+ errors.push(`Slide "${slideId}" has 'allFlags' requirement with empty flags array.`);
394
+ }
395
+ break;
396
+
397
+ default:
398
+ warnings.push(`Slide "${slideId}" has unknown requirement type: "${type}".`);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Valid gating condition types
404
+ */
405
+ const VALID_GATING_CONDITION_TYPES = [
406
+ 'objectiveStatus',
407
+ 'assessmentStatus',
408
+ 'assessmentAttempts',
409
+ 'assessmentConfig',
410
+ 'stateFlag',
411
+ 'timeOnSlide',
412
+ 'custom'
413
+ ];
414
+
415
+ /**
416
+ * Validates navigation gating conditions for a slide.
417
+ * @param {string} slideId - The slide identifier
418
+ * @param {object} gating - The gating configuration object
419
+ * @param {Set<string>} objectiveIds - Set of valid objective IDs
420
+ * @param {array} errors - Array to collect errors
421
+ */
422
+ export function validateGatingConditions(slideId, gating, objectiveIds, errors) {
423
+ if (!gating.conditions || !Array.isArray(gating.conditions)) {
424
+ errors.push(`Slide "${slideId}" has navigation.gating but no conditions array.`);
425
+ return;
426
+ }
427
+
428
+ if (gating.mode && !['all', 'any'].includes(gating.mode)) {
429
+ errors.push(`Slide "${slideId}" has invalid gating.mode "${gating.mode}". Must be "all" or "any".`);
430
+ }
431
+
432
+ for (const condition of gating.conditions) {
433
+ if (!condition.type) {
434
+ errors.push(`Slide "${slideId}" has a gating condition without a type property.`);
435
+ continue;
436
+ }
437
+
438
+ if (!VALID_GATING_CONDITION_TYPES.includes(condition.type)) {
439
+ errors.push(
440
+ `Slide "${slideId}" has invalid gating condition type: "${condition.type}". ` +
441
+ `Valid types: ${VALID_GATING_CONDITION_TYPES.join(', ')}. ` +
442
+ 'Note: \'slideVisited\' is only valid for objective criteria, not gating conditions.'
443
+ );
444
+ continue;
445
+ }
446
+
447
+ switch (condition.type) {
448
+ case 'objectiveStatus':
449
+ if (!condition.objectiveId) {
450
+ errors.push(`Slide "${slideId}" has 'objectiveStatus' gating condition without objectiveId.`);
451
+ } else if (!objectiveIds.has(condition.objectiveId)) {
452
+ errors.push(`Slide "${slideId}" has 'objectiveStatus' gating condition with unknown objectiveId: "${condition.objectiveId}".`);
453
+ }
454
+ break;
455
+
456
+ case 'assessmentStatus':
457
+ case 'assessmentAttempts':
458
+ case 'assessmentConfig':
459
+ if (!condition.assessmentId) {
460
+ errors.push(`Slide "${slideId}" has '${condition.type}' gating condition without assessmentId.`);
461
+ }
462
+ break;
463
+
464
+ case 'stateFlag':
465
+ if (!condition.key) {
466
+ errors.push(`Slide "${slideId}" has 'stateFlag' gating condition without key property.`);
467
+ }
468
+ break;
469
+
470
+ case 'timeOnSlide':
471
+ if (!condition.minSeconds && condition.minSeconds !== 0) {
472
+ errors.push(`Slide "${slideId}" has 'timeOnSlide' gating condition without minSeconds property.`);
473
+ }
474
+ break;
475
+
476
+ case 'custom':
477
+ if (!condition.callback && typeof condition.evaluate !== 'function') {
478
+ errors.push(`Slide "${slideId}" has 'custom' gating condition without callback or evaluate function.`);
479
+ }
480
+ break;
481
+ }
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Format lint results for display.
487
+ * @param {{ errors: string[], warnings: string[] }} results
488
+ * @returns {string} Formatted output
489
+ */
490
+ export function formatLintResults({ errors, warnings }) {
491
+ const lines = [];
492
+
493
+ if (warnings.length > 0) {
494
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
495
+ lines.push(' COURSE VALIDATION WARNINGS');
496
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
497
+ lines.push('');
498
+ warnings.forEach((w, i) => lines.push(`${i + 1}. ${w}`));
499
+ lines.push('');
500
+ }
501
+
502
+ if (errors.length > 0) {
503
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
504
+ lines.push(' COURSE VALIDATION FAILED');
505
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
506
+ lines.push('');
507
+ errors.forEach((e, i) => lines.push(`${i + 1}. ${e}`));
508
+ lines.push('');
509
+ lines.push('The course cannot be built until these errors are resolved.');
510
+ }
511
+
512
+ if (errors.length === 0 && warnings.length === 0) {
513
+ lines.push('✅ Course validation passed with no errors or warnings.');
514
+ }
515
+
516
+ return lines.join('\n');
517
+ }