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,174 @@
1
+ /**
2
+ * @file navigation-validators.js
3
+ * @description Validation functions for navigation access control.
4
+ * All validators return consistent {allowed: boolean, message: string|null, reason?: string} objects.
5
+ * @author Seth
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import { evaluateGatingCondition, shouldBypassGating } from './navigation-helpers.js';
10
+ import * as AssessmentManager from '../managers/assessment-manager.js';
11
+
12
+ /**
13
+ * Determines whether a slide should be included in the sequential navigation flow.
14
+ * Checks sequence configuration and evaluates dynamic conditions.
15
+ *
16
+ * SMART DEFAULT: For slides with gating conditions AND hidden from menu (menu.hidden: true),
17
+ * the gating conditions are automatically used for sequence inclusion. This prevents
18
+ * navigation loops when gating is bypassed for testing, without requiring duplicate config.
19
+ *
20
+ * @param {object} slide - The slide entry containing navigation configuration
21
+ * @param {object} stateManager - StateManager instance for reading state
22
+ * @param {Map} assessmentConfigs - Map of assessment configurations
23
+ * @returns {boolean} True when the slide should be part of the active sequence
24
+ */
25
+ export function isSlideInSequence(slide, stateManager, assessmentConfigs) {
26
+ if (!slide) {
27
+ return false;
28
+ }
29
+
30
+ const sequence = slide.navigation?.sequence;
31
+ const gating = slide.navigation?.gating;
32
+ const isHiddenFromMenu = slide.menu?.hidden === true;
33
+
34
+ // SMART DEFAULT: If no explicit sequence config, but slide has gating AND is hidden,
35
+ // use gating conditions to determine sequence inclusion.
36
+ // This prevents loops when gating is bypassed (e.g., remedial slides).
37
+ if (!sequence && gating?.conditions?.length > 0 && isHiddenFromMenu) {
38
+ const gatingMode = gating.mode || 'all';
39
+ if (gatingMode === 'any') {
40
+ return gating.conditions.some(condition =>
41
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
42
+ );
43
+ }
44
+ // Default to 'all' for any mode value (including invalid ones)
45
+ return gating.conditions.every(condition =>
46
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
47
+ );
48
+ }
49
+
50
+ // No sequence config and no smart default applies = always included
51
+ if (!sequence) {
52
+ return true;
53
+ }
54
+
55
+ const includeWhen = sequence.includeWhen || [];
56
+ const skipUntil = sequence.skipUntil || [];
57
+ const includeByDefault = sequence.includeByDefault !== false;
58
+
59
+ let include = includeByDefault;
60
+
61
+ // Check includeWhen conditions
62
+ if (includeWhen.length > 0) {
63
+ include = includeWhen.every(condition =>
64
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
65
+ );
66
+ }
67
+
68
+ // Check skipUntil conditions
69
+ if (include && skipUntil.length > 0) {
70
+ const skipSatisfied = skipUntil.every(condition =>
71
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
72
+ );
73
+ if (!skipSatisfied) {
74
+ include = false;
75
+ }
76
+ }
77
+
78
+ return include;
79
+ }
80
+
81
+ /**
82
+ * Validates whether a slide is accessible based on gating conditions.
83
+ * Checks all configured gating rules and applies dev mode override.
84
+ *
85
+ * @param {object} slide - The slide entry with navigation configuration
86
+ * @param {object} stateManager - StateManager instance
87
+ * @param {Map} assessmentConfigs - Map of assessment configurations
88
+ * @returns {{allowed: boolean, message: string|null, reason?: string}} Access result
89
+ */
90
+ export function validateSlideAccess(slide, stateManager, assessmentConfigs) {
91
+ // Dev mode gating override
92
+ if (shouldBypassGating()) {
93
+ return { allowed: true, message: null, reason: 'dev-bypass' };
94
+ }
95
+
96
+ if (!slide || !slide.navigation || !slide.navigation.gating) {
97
+ return { allowed: true, message: null, reason: null };
98
+ }
99
+
100
+ const gating = slide.navigation.gating;
101
+ const conditions = gating.conditions || [];
102
+
103
+ if (conditions.length === 0) {
104
+ return { allowed: true, message: null, reason: null };
105
+ }
106
+
107
+ const mode = gating.mode || 'all';
108
+ let allowed;
109
+
110
+ if (mode === 'any') {
111
+ // At least one condition must be met
112
+ allowed = conditions.some(condition =>
113
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
114
+ );
115
+ } else {
116
+ // Default to 'all' — all conditions must be met (including invalid mode values)
117
+ allowed = conditions.every(condition =>
118
+ evaluateGatingCondition(condition, stateManager, assessmentConfigs)
119
+ );
120
+ }
121
+
122
+ return {
123
+ allowed,
124
+ message: allowed ? null : (gating.message || 'This content is currently locked.'),
125
+ reason: allowed ? null : 'gating-failed'
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Validates whether navigation FROM the current slide is allowed.
131
+ * Checks assessment completion requirements.
132
+ *
133
+ * @param {object} slide - The slide entry to check
134
+ * @param {Map} assessmentConfigs - Map of assessment configurations
135
+ * @returns {{allowed: boolean, message: string|null, reason?: string}} Navigation permission result
136
+ */
137
+ export function validateNavigationFrom(slide, assessmentConfigs) {
138
+ // Dev mode override
139
+ if (shouldBypassGating()) {
140
+ return { allowed: true, message: null, reason: 'dev-bypass' };
141
+ }
142
+
143
+ if (!slide) {
144
+ return { allowed: true, message: null };
145
+ }
146
+
147
+ // Check if this is an assessment with completion requirements that block navigation
148
+ if (slide.type === 'assessment') {
149
+ const config = assessmentConfigs.get(slide.assessmentId);
150
+ const requirements = config?.completionRequirements;
151
+
152
+ // Only block if blockNavigation is explicitly true
153
+ if (requirements?.blockNavigation === true) {
154
+ const requirementsMet = AssessmentManager.meetsCompletionRequirements(
155
+ slide.assessmentId,
156
+ requirements
157
+ );
158
+
159
+ if (!requirementsMet) {
160
+ // Build helpful message based on what's required
161
+ let message = 'You must complete the assessment before continuing.';
162
+ if (requirements.requirePass) {
163
+ message = 'You must pass the assessment before continuing.';
164
+ } else if (requirements.requireSubmission) {
165
+ message = 'You must submit your assessment before continuing.';
166
+ }
167
+
168
+ return { allowed: false, message, reason: 'assessment-incomplete' };
169
+ }
170
+ }
171
+ }
172
+
173
+ return { allowed: true, message: null };
174
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @file state/index.js
3
+ * @description Barrel export for the state module.
4
+ * stateManager is the sole public API — all LMS and state operations flow through it.
5
+ */
6
+
7
+ export { default } from './state-manager.js';
8
+ export { formatISO8601Duration } from './state-manager.js';
@@ -0,0 +1,482 @@
1
+ /**
2
+ * @file lms-connection.js
3
+ * @description Manages LMS connection lifecycle using format-specific drivers.
4
+ * Handles initialization, termination, keep-alive, and emergency save on unload.
5
+ * Provides semantic passthrough to the active driver.
6
+ *
7
+ * INTERNAL: This is an internal module. External code should use stateManager
8
+ * as the sole public API for state operations.
9
+ */
10
+
11
+ import { logger } from '../utilities/logger.js';
12
+ import { eventBus } from '../core/event-bus.js';
13
+ import stateManager from './state-manager.js';
14
+ import { createDriver } from '../drivers/driver-factory.js';
15
+ import { classifyLmsError } from './lms-error-utils.js';
16
+
17
+ /**
18
+ * Get the LMS format using runtime detection.
19
+ * Priority: <meta name="lms-format"> → build-time env → 'cmi5' default.
20
+ *
21
+ * The meta tag is the primary mechanism — it's stamped into index.html at
22
+ * build/packaging time so a single universal build can serve any format.
23
+ * The cloud (or ZIP packaging) can re-stamp it without re-running Vite.
24
+ */
25
+ function getLMSFormat() {
26
+ // 1. Runtime: <meta name="lms-format"> in the HTML (stamped at build or by cloud)
27
+ if (typeof document !== 'undefined') {
28
+ const metaEl = document.querySelector('meta[name="lms-format"]');
29
+ if (metaEl?.content) {
30
+ return metaEl.content;
31
+ }
32
+ }
33
+
34
+ // 2. Build-time: Vite define (still useful for preview server / dev builds)
35
+ if (import.meta.env.LMS_FORMAT) {
36
+ return import.meta.env.LMS_FORMAT;
37
+ }
38
+
39
+ // 3. Default
40
+ return 'cmi5';
41
+ }
42
+
43
+ class LMSConnection {
44
+ constructor() {
45
+ // Create the appropriate driver based on format
46
+ this.format = getLMSFormat();
47
+ this.driver = null; // Lazy initialization via async _getDriver()
48
+
49
+ // Keep-alive interval handle
50
+ this.keepAliveInterval = null;
51
+
52
+ // Session timing
53
+ this.sessionStartTime = 0;
54
+
55
+ // Compatibility profile (influences timeout behavior and guardrails)
56
+ this.compatibilityMode = 'auto';
57
+
58
+ // Operation diagnostics
59
+ this.diagnostics = {
60
+ profile: 'balanced',
61
+ lastSuccessAt: null,
62
+ operationCounts: {
63
+ commitSuccess: 0,
64
+ commitFailure: 0,
65
+ terminateSuccess: 0,
66
+ terminateFailure: 0
67
+ }
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Gets the driver instance, creating it lazily if needed.
73
+ * Must be called after initialize() for non-SCORM2004 formats.
74
+ * @returns {LMSDriver} The driver instance
75
+ * @throws {Error} If driver not initialized
76
+ */
77
+ _getDriver() {
78
+ if (!this.driver) {
79
+ throw new Error('[LMSConnection] Driver not initialized. Call initialize() first.');
80
+ }
81
+ return this.driver;
82
+ }
83
+
84
+ // ============================================================================
85
+ // Public API
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Gets the current LMS format.
90
+ * @returns {string} 'scorm2004' | 'scorm1.2' | 'cmi5' | 'lti'
91
+ */
92
+ getFormat() {
93
+ return this.format;
94
+ }
95
+
96
+ /**
97
+ * Checks if connected to LMS.
98
+ * @returns {boolean}
99
+ */
100
+ get isConnected() {
101
+ return this.driver?.isConnected() ?? false;
102
+ }
103
+
104
+ /**
105
+ * Checks if session is terminated.
106
+ * @returns {boolean}
107
+ */
108
+ get isTerminated() {
109
+ return this.driver?.isTerminated() ?? false;
110
+ }
111
+
112
+ /**
113
+ * Gets session start time.
114
+ * @returns {number} Timestamp
115
+ */
116
+ get sessionStart() {
117
+ return this.sessionStartTime;
118
+ }
119
+
120
+ /**
121
+ * Initializes the LMS connection.
122
+ * @returns {Promise<boolean>} True if connected
123
+ */
124
+ async initialize() {
125
+ this.sessionStartTime = Date.now();
126
+
127
+ // Create driver asynchronously (allows dynamic import for cmi5/scorm12)
128
+ this.driver = await createDriver(this.format);
129
+ logger.debug(`[LMSConnection] Created ${this.format} driver`);
130
+
131
+ const result = await this.driver.initialize();
132
+
133
+ if (result) {
134
+ this._startKeepAlive();
135
+ logger.debug(`[LMSConnection] Initialized with ${this.format} driver`);
136
+ }
137
+
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Terminates the LMS connection.
143
+ * @returns {Promise<boolean>} True if successful
144
+ */
145
+ async terminate() {
146
+ this._stopKeepAlive();
147
+
148
+ const driver = this._getDriver();
149
+ const timeoutMs = this._getOperationTimeoutMs('terminate');
150
+ try {
151
+ const result = await this._withTimeout(driver.terminate(), timeoutMs, 'terminate');
152
+ this._markOperationSuccess('terminate');
153
+ return result;
154
+ } catch (error) {
155
+ this._markOperationFailure('terminate', error);
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Gets driver capabilities.
162
+ * @returns {Object} Capabilities declaration
163
+ */
164
+ getCapabilities() {
165
+ return this._getDriver().getCapabilities();
166
+ }
167
+
168
+ // --- Semantic Reads (passthrough to driver) ---
169
+
170
+ getEntryMode() {
171
+ return this._getDriver().getEntryMode();
172
+ }
173
+
174
+ getBookmark() {
175
+ return this._getDriver().getBookmark();
176
+ }
177
+
178
+ getCompletion() {
179
+ return this._getDriver().getCompletion();
180
+ }
181
+
182
+ getSuccess() {
183
+ return this._getDriver().getSuccess();
184
+ }
185
+
186
+ getScore() {
187
+ return this._getDriver().getScore();
188
+ }
189
+
190
+ getLearnerInfo() {
191
+ return this._getDriver().getLearnerInfo();
192
+ }
193
+
194
+ // --- Semantic Writes (passthrough to driver) ---
195
+
196
+ setBookmark(location) {
197
+ return this._getDriver().setBookmark(location);
198
+ }
199
+
200
+ reportScore(score) {
201
+ return this._getDriver().reportScore(score);
202
+ }
203
+
204
+ reportCompletion(status) {
205
+ return this._getDriver().reportCompletion(status);
206
+ }
207
+
208
+ reportSuccess(status) {
209
+ return this._getDriver().reportSuccess(status);
210
+ }
211
+
212
+ reportProgress(measure) {
213
+ return this._getDriver().reportProgress(measure);
214
+ }
215
+
216
+ reportSessionTime(duration) {
217
+ return this._getDriver().reportSessionTime(duration);
218
+ }
219
+
220
+ reportObjective(objective) {
221
+ return this._getDriver().reportObjective(objective);
222
+ }
223
+
224
+ reportInteraction(interaction) {
225
+ return this._getDriver().reportInteraction(interaction);
226
+ }
227
+
228
+ setExitMode(mode) {
229
+ return this._getDriver().setExitMode(mode);
230
+ }
231
+
232
+ // --- Data Persistence ---
233
+
234
+ /**
235
+ * Commits buffered writes to the LMS.
236
+ * @returns {Promise<boolean>} True if successful
237
+ */
238
+ async commit() {
239
+ const timeoutMs = this._getOperationTimeoutMs('commit');
240
+ try {
241
+ const result = await this._withTimeout(this._getDriver().commit(), timeoutMs, 'commit');
242
+ this._markOperationSuccess('commit');
243
+ return result;
244
+ } catch (error) {
245
+ this._markOperationFailure('commit', error);
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Gets parsed suspend_data from the LMS.
252
+ * @returns {object|null} Parsed suspend data
253
+ */
254
+ getSuspendData() {
255
+ return this._getDriver().getSuspendData();
256
+ }
257
+
258
+ /**
259
+ * Sets suspend_data in the LMS.
260
+ * @param {object} data - The data to store
261
+ * @returns {boolean} True if successful
262
+ */
263
+ setSuspendData(data) {
264
+ return this._getDriver().setSuspendData(data);
265
+ }
266
+
267
+ /**
268
+ * Gets cmi5 launch data (moveOn, masteryScore, etc.).
269
+ * Only available for cmi5 format.
270
+ * @returns {Object|null} Launch data or null if not available/not cmi5
271
+ */
272
+ getLaunchData() {
273
+ const driver = this.driver;
274
+ if (!driver || typeof driver.getLaunchData !== 'function') {
275
+ return null;
276
+ }
277
+ return driver.getLaunchData();
278
+ }
279
+
280
+ /**
281
+ * Gets the underlying driver instance.
282
+ * Used by xAPI statement service to access driver-specific methods.
283
+ * @returns {LMSDriver|null} The driver instance or null if not initialized
284
+ */
285
+ getDriver() {
286
+ return this.driver;
287
+ }
288
+
289
+ /**
290
+ * Sets compatibility mode to tune reliability behavior by LMS profile.
291
+ * Must be called before initialize() for deterministic startup behavior.
292
+ * @param {'auto'|'balanced'|'strict-scorm12'|'conservative-scorm2004'|'modern-http'} mode
293
+ */
294
+ setCompatibilityMode(mode = 'auto') {
295
+ const validModes = new Set([
296
+ 'auto',
297
+ 'balanced',
298
+ 'strict-scorm12',
299
+ 'conservative-scorm2004',
300
+ 'modern-http'
301
+ ]);
302
+
303
+ if (!validModes.has(mode)) {
304
+ throw new Error(`[LMSConnection] Invalid compatibility mode "${mode}". Expected one of: ${Array.from(validModes).join(', ')}`);
305
+ }
306
+
307
+ this.compatibilityMode = mode;
308
+ }
309
+
310
+ getCompatibilityMode() {
311
+ return this.compatibilityMode;
312
+ }
313
+
314
+ getDiagnostics() {
315
+ return { ...this.diagnostics, operationCounts: { ...this.diagnostics.operationCounts } };
316
+ }
317
+
318
+ // ============================================================================
319
+ // Lifecycle Handlers
320
+ // ============================================================================
321
+
322
+ /**
323
+ * Sets up lifecycle handlers for page unload/hide events.
324
+ */
325
+ setupLifecycleHandlers() {
326
+ // Layer 4: Auto-Terminate on Unload (Emergency Save)
327
+ window.addEventListener('pagehide', () => {
328
+ if (this.isTerminated) return;
329
+
330
+ // Startup Guard: Ignore pagehide events < 2s after startup
331
+ if (Date.now() - this.sessionStartTime < 2000) {
332
+ logger.debug('[LMSConnection] Page unload detected immediately after startup (< 2s). Ignoring.');
333
+ return;
334
+ }
335
+
336
+ logger.debug('[LMSConnection] Page unload detected. Attempting emergency save...');
337
+
338
+ // Use emergencySave() for any driver that supports it (sendBeacon guaranteed delivery)
339
+ if (this.driver?.emergencySave) {
340
+ try {
341
+ this.driver.emergencySave();
342
+ logger.debug('[LMSConnection] Emergency save completed via sendBeacon');
343
+ } catch (e) {
344
+ logger.debug('[LMSConnection] Emergency save failed:', e.message);
345
+ }
346
+ return;
347
+ }
348
+
349
+ // For SCORM formats, use synchronous exitCourseWithSuspend
350
+ try {
351
+ // Delegate to StateManager to save exit status
352
+ const exitPromise = stateManager.exitCourseWithSuspend();
353
+ if (exitPromise && typeof exitPromise.catch === 'function') {
354
+ exitPromise.catch(e => {
355
+ logger.debug('[LMSConnection] Best-effort save during unload did not complete:', e.message);
356
+ });
357
+ }
358
+ } catch (e) {
359
+ logger.debug('[LMSConnection] Best-effort save during unload did not complete:', e.message);
360
+ }
361
+ });
362
+ }
363
+
364
+ // ============================================================================
365
+ // Private Helpers
366
+ // ============================================================================
367
+
368
+ _startKeepAlive() {
369
+ this._stopKeepAlive();
370
+
371
+ // Only SCORM formats need keep-alive (cmi5/LTI use stateless HTTP)
372
+ if (this.format.startsWith('cmi5') || this.format === 'lti') {
373
+ return;
374
+ }
375
+
376
+ const KEEP_ALIVE_INTERVAL = 10 * 60 * 1000; // 10 minutes
377
+
378
+ this.keepAliveInterval = setInterval(() => {
379
+ if (!this.isConnected || this.isTerminated) {
380
+ this._stopKeepAlive();
381
+ return;
382
+ }
383
+ // Delegate to driver's ping method
384
+ this.driver?.ping?.();
385
+ }, KEEP_ALIVE_INTERVAL);
386
+
387
+ logger.debug('[LMSConnection] Keep-alive mechanism started (10m interval)');
388
+ }
389
+
390
+ _stopKeepAlive() {
391
+ if (this.keepAliveInterval) {
392
+ clearInterval(this.keepAliveInterval);
393
+ this.keepAliveInterval = null;
394
+ }
395
+ }
396
+
397
+ _resolveCompatibilityProfile() {
398
+ if (this.compatibilityMode !== 'auto') {
399
+ return this.compatibilityMode;
400
+ }
401
+
402
+ if (this.format === 'scorm1.2') return 'strict-scorm12';
403
+ if (this.format === 'scorm2004') return 'conservative-scorm2004';
404
+ if (this.format.startsWith('cmi5') || this.format === 'lti') return 'modern-http';
405
+ return 'balanced';
406
+ }
407
+
408
+ _getOperationTimeoutMs(operation) {
409
+ const profile = this._resolveCompatibilityProfile();
410
+ this.diagnostics.profile = profile;
411
+
412
+ const timeoutMatrix = {
413
+ balanced: { commit: 8000, terminate: 10000 },
414
+ 'strict-scorm12': { commit: 5000, terminate: 7000 },
415
+ 'conservative-scorm2004': { commit: 7000, terminate: 9000 },
416
+ 'modern-http': { commit: 12000, terminate: 15000 }
417
+ };
418
+
419
+ const profileTimeouts = timeoutMatrix[profile] || timeoutMatrix.balanced;
420
+ return profileTimeouts[operation] || 8000;
421
+ }
422
+
423
+ async _withTimeout(promise, timeoutMs, operation) {
424
+ let timeoutHandle;
425
+ const timeoutPromise = new Promise((_, reject) => {
426
+ timeoutHandle = setTimeout(() => {
427
+ reject(new Error(`[LMSConnection] ${operation} timed out after ${timeoutMs}ms`));
428
+ }, timeoutMs);
429
+ });
430
+
431
+ try {
432
+ return await Promise.race([promise, timeoutPromise]);
433
+ } finally {
434
+ clearTimeout(timeoutHandle);
435
+ }
436
+ }
437
+
438
+ _markOperationSuccess(operation) {
439
+ this.diagnostics.lastSuccessAt = new Date().toISOString();
440
+ if (operation === 'commit') {
441
+ this.diagnostics.operationCounts.commitSuccess++;
442
+ } else if (operation === 'terminate') {
443
+ this.diagnostics.operationCounts.terminateSuccess++;
444
+ }
445
+ }
446
+
447
+ _markOperationFailure(operation, error) {
448
+ const classification = classifyLmsError(error);
449
+ const message = error?.message || String(error);
450
+ const context = {
451
+ domain: 'lms',
452
+ operation,
453
+ classification,
454
+ format: this.format,
455
+ profile: this.diagnostics.profile,
456
+ stack: error?.stack
457
+ };
458
+
459
+ if (operation === 'commit') {
460
+ this.diagnostics.operationCounts.commitFailure++;
461
+ } else if (operation === 'terminate') {
462
+ this.diagnostics.operationCounts.terminateFailure++;
463
+ }
464
+
465
+ logger.error(`[LMSConnection] ${operation} failed`, {
466
+ ...context,
467
+ message
468
+ });
469
+
470
+ eventBus.emit('lms:operationFailed', {
471
+ operation,
472
+ classification,
473
+ message,
474
+ format: context.format,
475
+ profile: context.profile
476
+ });
477
+ }
478
+ }
479
+
480
+ const lmsConnection = new LMSConnection();
481
+ export default lmsConnection;
482
+ export { LMSConnection };