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,768 @@
1
+ /**
2
+ * @file cmi5-driver.js
3
+ * @description cmi5 driver implementation using @xapi/cmi5 package.
4
+ * Extends HttpDriverBase for shared mock state, suspend data, and semantic interface.
5
+ *
6
+ * The @xapi/cmi5 package handles:
7
+ * - Authentication via fetch URL
8
+ * - Launch data retrieval
9
+ * - cmi5-defined statements (Initialized, Completed, Passed, Failed, Terminated)
10
+ * - Statement timing and duration calculations
11
+ *
12
+ * This driver adds:
13
+ * - xAPI statement methods (objectives, interactions, assessments, slides)
14
+ * - suspend_data persistence via xAPI State API
15
+ * - Bookmark persistence via xAPI State API
16
+ * - Emergency save via sendBeacon
17
+ */
18
+
19
+ import { HttpDriverBase } from './http-driver-base.js';
20
+ import { logger } from '../utilities/logger.js';
21
+
22
+ // @xapi/cmi5 is dynamically imported in initialize() to avoid bundling for non-cmi5 builds
23
+
24
+ // State document IDs for xAPI State API
25
+ const STATE_ID_SUSPEND_DATA = 'https://w3id.org/xapi/cmi5/state/suspend_data';
26
+ const STATE_ID_BOOKMARK = 'https://w3id.org/xapi/cmi5/state/bookmark';
27
+
28
+ // =============================================================================
29
+ // cmi5 Driver Class
30
+ // =============================================================================
31
+
32
+ export class Cmi5Driver extends HttpDriverBase {
33
+ constructor() {
34
+ super();
35
+ this._cmi5 = null; // @xapi/cmi5 instance
36
+
37
+ // Track what statements we've sent (persisted for resume)
38
+ this._sentComplete = false;
39
+ this._sentResult = false;
40
+ }
41
+
42
+ // =========================================================================
43
+ // Interface Implementation
44
+ // =========================================================================
45
+
46
+ getFormat() {
47
+ return 'cmi5';
48
+ }
49
+
50
+ getCapabilities() {
51
+ return {
52
+ supportsObjectives: true, // via suspend_data
53
+ supportsInteractions: true, // via suspend_data
54
+ supportsComments: true, // via suspend_data
55
+ supportsEmergencySave: true,
56
+ maxSuspendDataBytes: 0, // unlimited (LRS-dependent)
57
+ asyncCommit: true
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Initializes the cmi5 connection.
63
+ */
64
+ async initialize() {
65
+ if (this._isConnected) {
66
+ return true;
67
+ }
68
+
69
+ // Check for cmi5 dev API (stub player or standalone preview)
70
+ // Search current window and parent frame (stub player injects on parent)
71
+ const devApi = typeof window !== 'undefined' && (window.cmi5 || (window.parent !== window && window.parent.cmi5));
72
+ if (devApi) {
73
+ logger.info('[Cmi5Driver] Using cmi5 development API');
74
+ this._mock = true;
75
+ this._devApi = devApi;
76
+ this._devApi.initialize();
77
+ this._loadMockState();
78
+ this._isConnected = true;
79
+ this._logMockStatement('initialized', { verb: 'initialized' });
80
+ return true;
81
+ }
82
+
83
+ // Check for cmi5 launch parameters
84
+ if (!this._hasLaunchParameters()) {
85
+ logger.info('[Cmi5Driver] No cmi5 launch parameters. Using localStorage mock.');
86
+ this._mock = true;
87
+ this._loadMockState();
88
+ this._isConnected = true;
89
+ this._logMockStatement('initialized', { verb: 'initialized' });
90
+ return true;
91
+ }
92
+
93
+ // Production mode: dynamically import @xapi/cmi5
94
+ try {
95
+ const Cmi5Module = await import('@xapi/cmi5');
96
+ const Cmi5 = Cmi5Module.default || Cmi5Module.Cmi5 || Cmi5Module;
97
+
98
+ if (!Cmi5 || typeof Cmi5 !== 'function') {
99
+ throw new Error('Failed to load Cmi5 class from @xapi/cmi5 module');
100
+ }
101
+
102
+ if (typeof Cmi5.instance !== 'undefined') {
103
+ this._cmi5 = Cmi5.instance;
104
+ } else if (typeof Cmi5.isCmiAvailable !== 'undefined' && Cmi5.isCmiAvailable) {
105
+ this._cmi5 = new Cmi5();
106
+ } else {
107
+ this._cmi5 = new Cmi5(this._getLaunchParametersFromURL());
108
+ }
109
+
110
+ await this._cmi5.initialize();
111
+ await this._prefetchState();
112
+
113
+ this._isConnected = true;
114
+ logger.debug('[Cmi5Driver] Initialized via @xapi/cmi5');
115
+ return true;
116
+
117
+ } catch (error) {
118
+ if (import.meta.env.DEV) {
119
+ logger.warn('[Cmi5Driver] @xapi/cmi5 unavailable, using mock mode:', error.message);
120
+ this._mock = true;
121
+ this._loadMockState();
122
+ this._isConnected = true;
123
+ return true;
124
+ }
125
+
126
+ const isExpiredToken = error.message?.includes('400') ||
127
+ error.response?.status === 400 ||
128
+ error.message?.includes('fetch');
129
+
130
+ if (isExpiredToken) {
131
+ const sessionError = new Error(
132
+ 'Your session has expired. Please return to the learning management system to resume this course.'
133
+ );
134
+ sessionError.isSessionExpired = true;
135
+ sessionError.userFacing = true;
136
+ throw sessionError;
137
+ }
138
+
139
+ throw new Error(`[Cmi5Driver] Initialization failed: ${error.message}`);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Terminates the cmi5 session.
145
+ */
146
+ async terminate() {
147
+ if (!this._isConnected || this._isTerminated) {
148
+ return true;
149
+ }
150
+
151
+ if (this._mock) {
152
+ return this._terminateMock();
153
+ }
154
+
155
+ try {
156
+ await this._persistState();
157
+
158
+ if (this._completionStatus === 'completed' && !this._sentComplete) {
159
+ await this._cmi5.complete();
160
+ this._sentComplete = true;
161
+ logger.debug('[Cmi5Driver] Sent Completed statement');
162
+ }
163
+
164
+ if (!this._sentResult) {
165
+ const scoreObj = this._score !== null ? { scaled: this._score } : undefined;
166
+
167
+ if (this._successStatus === 'passed') {
168
+ await this._cmi5.pass(scoreObj);
169
+ this._sentResult = true;
170
+ logger.debug('[Cmi5Driver] Sent Passed statement');
171
+ } else if (this._successStatus === 'failed') {
172
+ await this._cmi5.fail(scoreObj);
173
+ this._sentResult = true;
174
+ logger.debug('[Cmi5Driver] Sent Failed statement');
175
+ }
176
+ }
177
+
178
+ await this._persistSentFlags();
179
+ await this._cmi5.terminate();
180
+ logger.debug('[Cmi5Driver] Sent Terminated statement');
181
+
182
+ this._isTerminated = true;
183
+ return true;
184
+
185
+ } catch (error) {
186
+ logger.error('[Cmi5Driver] Terminate failed:', error);
187
+ throw new Error(`[Cmi5Driver] Termination failed: ${error.message}`);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Emergency save using sendBeacon for page unload scenarios.
193
+ */
194
+ emergencySave() {
195
+ if (this._mock || this._isTerminated) {
196
+ if (this._mock) {
197
+ this._saveMockState();
198
+ }
199
+ return;
200
+ }
201
+
202
+ if (!this._cmi5) {
203
+ logger.warn('[Cmi5Driver] emergencySave called but cmi5 not initialized');
204
+ return;
205
+ }
206
+
207
+ const params = this._cmi5.getLaunchParameters();
208
+ if (!params) {
209
+ logger.warn('[Cmi5Driver] emergencySave: No launch parameters available');
210
+ return;
211
+ }
212
+
213
+ const endpoint = params.endpoint?.replace(/\/$/, '');
214
+ const registration = params.registration;
215
+ const activityId = encodeURIComponent(params.activityId);
216
+ const agent = encodeURIComponent(JSON.stringify(params.actor));
217
+ const stateId = encodeURIComponent(STATE_ID_SUSPEND_DATA);
218
+
219
+ if (this._suspendDataDirty && this._suspendDataCache !== null) {
220
+ const suspendDataUrl = `${endpoint}/activities/state?stateId=${stateId}&activityId=${activityId}&agent=${agent}&registration=${registration}`;
221
+ const suspendDataBlob = new Blob([JSON.stringify(this._suspendDataCache)], { type: 'application/json' });
222
+
223
+ const suspendSent = navigator.sendBeacon(suspendDataUrl, suspendDataBlob);
224
+ if (suspendSent) {
225
+ logger.debug('[Cmi5Driver] Emergency save: suspend_data sent via sendBeacon');
226
+ } else {
227
+ logger.warn('[Cmi5Driver] Emergency save: sendBeacon failed for suspend_data');
228
+ }
229
+ }
230
+
231
+ if (this._bookmarkDirty) {
232
+ const bookmarkStateId = encodeURIComponent(STATE_ID_BOOKMARK);
233
+ const bookmarkUrl = `${endpoint}/activities/state?stateId=${bookmarkStateId}&activityId=${activityId}&agent=${agent}&registration=${registration}`;
234
+ const bookmarkData = {
235
+ location: this._bookmarkCache,
236
+ completionStatus: this._completionStatus,
237
+ successStatus: this._successStatus,
238
+ score: this._score,
239
+ sentComplete: this._sentComplete,
240
+ sentResult: this._sentResult
241
+ };
242
+ const bookmarkBlob = new Blob([JSON.stringify(bookmarkData)], { type: 'application/json' });
243
+
244
+ const bookmarkSent = navigator.sendBeacon(bookmarkUrl, bookmarkBlob);
245
+ if (bookmarkSent) {
246
+ logger.debug('[Cmi5Driver] Emergency save: bookmark sent via sendBeacon');
247
+ } else {
248
+ logger.warn('[Cmi5Driver] Emergency save: sendBeacon failed for bookmark');
249
+ }
250
+ }
251
+ }
252
+
253
+ // =========================================================================
254
+ // Semantic Reads (override)
255
+ // =========================================================================
256
+
257
+ getLearnerInfo() {
258
+ if (this._cmi5) {
259
+ const params = this._cmi5.getLaunchParameters();
260
+ return {
261
+ id: params?.actor?.mbox || params?.actor?.account?.name || '',
262
+ name: params?.actor?.name || ''
263
+ };
264
+ }
265
+ return { id: 'dev-learner', name: 'Development User' };
266
+ }
267
+
268
+ // =========================================================================
269
+ // xAPI Statement Methods (cmi5-specific)
270
+ // =========================================================================
271
+
272
+ getLaunchData() {
273
+ if (this._mock) {
274
+ if (this._devApi && typeof this._devApi.getLaunchData === 'function') {
275
+ return this._devApi.getLaunchData();
276
+ }
277
+ return {
278
+ launchMode: 'Normal',
279
+ moveOn: 'NotApplicable',
280
+ masteryScore: null,
281
+ activityId: 'mock-activity-id',
282
+ registration: 'mock-registration-id'
283
+ };
284
+ }
285
+
286
+ if (!this._cmi5) {
287
+ return null;
288
+ }
289
+
290
+ try {
291
+ const launchData = this._cmi5.getLaunchData();
292
+ const params = this._cmi5.getLaunchParameters();
293
+
294
+ return {
295
+ launchMode: launchData?.launchMode || 'Normal',
296
+ moveOn: launchData?.moveOn || 'NotApplicable',
297
+ masteryScore: launchData?.masteryScore ?? null,
298
+ activityId: params?.activityId || null,
299
+ registration: params?.registration || null,
300
+ returnURL: launchData?.returnURL || null,
301
+ entitlementKey: launchData?.entitlementKey || null
302
+ };
303
+ } catch (error) {
304
+ logger.warn('[Cmi5Driver] Error getting launch data:', error.message);
305
+ return null;
306
+ }
307
+ }
308
+
309
+ async sendObjectiveStatement(data) {
310
+ if (this._mock) {
311
+ this._logMockStatement('objective', data);
312
+ return;
313
+ }
314
+
315
+ if (!this._cmi5) {
316
+ throw new Error('Cannot send objective statement: cmi5 not initialized');
317
+ }
318
+
319
+ const params = this._cmi5.getLaunchParameters();
320
+ const xapi = this._cmi5.xapi;
321
+
322
+ const verbMap = {
323
+ 'completed': 'http://adlnet.gov/expapi/verbs/completed',
324
+ 'progressed': 'http://adlnet.gov/expapi/verbs/progressed',
325
+ 'passed': 'http://adlnet.gov/expapi/verbs/passed',
326
+ 'failed': 'http://adlnet.gov/expapi/verbs/failed'
327
+ };
328
+
329
+ const verbId = verbMap[data.verb] || verbMap['progressed'];
330
+ const verbDisplay = data.verb || 'progressed';
331
+
332
+ const statement = {
333
+ verb: {
334
+ id: verbId,
335
+ display: { 'en-US': verbDisplay }
336
+ },
337
+ object: {
338
+ id: `${params.activityId}/objectives/${data.id}`,
339
+ definition: {
340
+ type: 'http://adlnet.gov/expapi/activities/objective',
341
+ name: data.name ? { 'en-US': data.name } : { 'en-US': data.id }
342
+ }
343
+ },
344
+ context: {
345
+ registration: params.registration,
346
+ contextActivities: {
347
+ parent: [{ id: params.activityId }]
348
+ }
349
+ }
350
+ };
351
+
352
+ if (data.score !== undefined || data.duration) {
353
+ statement.result = {};
354
+ if (data.score !== undefined) {
355
+ statement.result.score = { scaled: data.score };
356
+ }
357
+ if (data.duration) {
358
+ statement.result.duration = data.duration;
359
+ }
360
+ if (data.verb === 'completed' || data.verb === 'passed') {
361
+ statement.result.completion = true;
362
+ }
363
+ if (data.verb === 'passed' || data.verb === 'failed') {
364
+ statement.result.success = data.verb === 'passed';
365
+ }
366
+ }
367
+
368
+ try {
369
+ await xapi.sendStatement(statement);
370
+ logger.debug(`[Cmi5Driver] Sent objective statement: ${data.verb} for ${data.id}`);
371
+ } catch (error) {
372
+ logger.error('[Cmi5Driver] Failed to send objective statement:', error);
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ async sendInteractionStatement(data) {
378
+ if (this._mock) {
379
+ this._logMockStatement('interaction', data);
380
+ // Forward interaction data to dev API for server-side tracking
381
+ if (this._devApi && typeof this._devApi.recordInteraction === 'function') {
382
+ this._devApi.recordInteraction(data);
383
+ }
384
+ return;
385
+ }
386
+
387
+ if (!this._cmi5) {
388
+ throw new Error('Cannot send interaction statement: cmi5 not initialized');
389
+ }
390
+
391
+ const params = this._cmi5.getLaunchParameters();
392
+ const xapi = this._cmi5.xapi;
393
+
394
+ const interactionTypeMap = {
395
+ 'choice': 'choice',
396
+ 'true-false': 'true-false',
397
+ 'fill-in': 'fill-in',
398
+ 'long-fill-in': 'long-fill-in',
399
+ 'matching': 'matching',
400
+ 'performance': 'performance',
401
+ 'sequencing': 'sequencing',
402
+ 'likert': 'likert',
403
+ 'numeric': 'numeric',
404
+ 'other': 'other'
405
+ };
406
+
407
+ const statement = {
408
+ verb: {
409
+ id: 'http://adlnet.gov/expapi/verbs/answered',
410
+ display: { 'en-US': 'answered' }
411
+ },
412
+ object: {
413
+ id: `${params.activityId}/interactions/${data.id}`,
414
+ definition: {
415
+ type: 'http://adlnet.gov/expapi/activities/cmi.interaction',
416
+ interactionType: interactionTypeMap[data.type] || 'other'
417
+ }
418
+ },
419
+ result: {
420
+ response: String(data.response),
421
+ success: data.correct
422
+ },
423
+ context: {
424
+ registration: params.registration,
425
+ contextActivities: {
426
+ parent: [{ id: params.activityId }]
427
+ }
428
+ }
429
+ };
430
+
431
+ if (data.description) {
432
+ statement.object.definition.description = { 'en-US': data.description };
433
+ }
434
+
435
+ if (data.duration) {
436
+ statement.result.duration = data.duration;
437
+ }
438
+
439
+ if (data.objectiveId) {
440
+ statement.context.contextActivities.other = [{
441
+ id: `${params.activityId}/objectives/${data.objectiveId}`,
442
+ definition: { type: 'http://adlnet.gov/expapi/activities/objective' }
443
+ }];
444
+ }
445
+
446
+ try {
447
+ await xapi.sendStatement(statement);
448
+ logger.debug(`[Cmi5Driver] Sent interaction statement: ${data.id} (${data.correct ? 'correct' : 'incorrect'})`);
449
+ } catch (error) {
450
+ logger.error('[Cmi5Driver] Failed to send interaction statement:', error);
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ async sendAssessmentStatement(data) {
456
+ if (this._mock) {
457
+ this._logMockStatement('assessment', data);
458
+ return;
459
+ }
460
+
461
+ if (!this._cmi5) {
462
+ throw new Error('Cannot send assessment statement: cmi5 not initialized');
463
+ }
464
+
465
+ const params = this._cmi5.getLaunchParameters();
466
+ const xapi = this._cmi5.xapi;
467
+
468
+ const statement = {
469
+ verb: {
470
+ id: 'http://adlnet.gov/expapi/verbs/completed',
471
+ display: { 'en-US': 'completed' }
472
+ },
473
+ object: {
474
+ id: `${params.activityId}/assessments/${data.id}`,
475
+ definition: {
476
+ type: 'http://adlnet.gov/expapi/activities/assessment',
477
+ name: data.name ? { 'en-US': data.name } : { 'en-US': data.id }
478
+ }
479
+ },
480
+ result: {
481
+ score: {
482
+ scaled: data.score,
483
+ raw: data.correctCount,
484
+ max: data.questionCount,
485
+ min: 0
486
+ },
487
+ success: data.passed,
488
+ completion: true
489
+ },
490
+ context: {
491
+ registration: params.registration,
492
+ contextActivities: {
493
+ parent: [{ id: params.activityId }]
494
+ },
495
+ extensions: {
496
+ 'https://w3id.org/xapi/cmi5/context/extensions/attemptNumber': data.attemptNumber
497
+ }
498
+ }
499
+ };
500
+
501
+ if (data.duration) {
502
+ statement.result.duration = data.duration;
503
+ }
504
+
505
+ try {
506
+ await xapi.sendStatement(statement);
507
+ logger.debug(`[Cmi5Driver] Sent assessment statement: ${data.id} (${data.passed ? 'passed' : 'failed'}) attempt ${data.attemptNumber}`);
508
+ } catch (error) {
509
+ logger.error('[Cmi5Driver] Failed to send assessment statement:', error);
510
+ throw error;
511
+ }
512
+ }
513
+
514
+ async sendSlideStatement(data) {
515
+ if (this._mock) {
516
+ this._logMockStatement('slide', data);
517
+ return;
518
+ }
519
+
520
+ if (!this._cmi5) {
521
+ throw new Error('Cannot send slide statement: cmi5 not initialized');
522
+ }
523
+
524
+ const params = this._cmi5.getLaunchParameters();
525
+ const xapi = this._cmi5.xapi;
526
+
527
+ const statement = {
528
+ verb: {
529
+ id: 'http://adlnet.gov/expapi/verbs/experienced',
530
+ display: { 'en-US': 'experienced' }
531
+ },
532
+ object: {
533
+ id: `${params.activityId}/slides/${data.id}`,
534
+ definition: {
535
+ type: 'http://adlnet.gov/expapi/activities/media',
536
+ name: data.title ? { 'en-US': data.title } : { 'en-US': data.id }
537
+ }
538
+ },
539
+ context: {
540
+ registration: params.registration,
541
+ contextActivities: {
542
+ parent: [{ id: params.activityId }]
543
+ }
544
+ }
545
+ };
546
+
547
+ if (data.duration) {
548
+ statement.result = {
549
+ duration: data.duration
550
+ };
551
+ }
552
+
553
+ try {
554
+ await xapi.sendStatement(statement);
555
+ logger.debug(`[Cmi5Driver] Sent slide statement: experienced ${data.id} (${data.duration || 'no duration'})`);
556
+ } catch (error) {
557
+ logger.error('[Cmi5Driver] Failed to send slide statement:', error);
558
+ throw error;
559
+ }
560
+ }
561
+
562
+ // =========================================================================
563
+ // Private: Launch Parameters
564
+ // =========================================================================
565
+
566
+ _hasLaunchParameters() {
567
+ const params = new URLSearchParams(window.location.search);
568
+ return Boolean(
569
+ params.get('fetch') &&
570
+ params.get('endpoint') &&
571
+ params.get('actor') &&
572
+ params.get('registration') &&
573
+ params.get('activityId')
574
+ );
575
+ }
576
+
577
+ _getLaunchParametersFromURL() {
578
+ const params = new URLSearchParams(window.location.search);
579
+ const launchParams = {
580
+ fetch: params.get('fetch'),
581
+ endpoint: params.get('endpoint'),
582
+ registration: params.get('registration'),
583
+ activityId: params.get('activityId')
584
+ };
585
+
586
+ const actorParam = params.get('actor');
587
+ if (actorParam) {
588
+ try {
589
+ launchParams.actor = JSON.parse(actorParam);
590
+ } catch {
591
+ launchParams.actor = actorParam;
592
+ }
593
+ }
594
+
595
+ return launchParams;
596
+ }
597
+
598
+ // =========================================================================
599
+ // Private: State Management via xAPI State API
600
+ // =========================================================================
601
+
602
+ async _prefetchState() {
603
+ const params = this._cmi5.getLaunchParameters();
604
+ const xapi = this._cmi5.xapi;
605
+
606
+ try {
607
+ const response = await xapi.getState({
608
+ agent: params.actor,
609
+ activityId: params.activityId,
610
+ stateId: STATE_ID_SUSPEND_DATA,
611
+ registration: params.registration
612
+ });
613
+ this._suspendDataCache = response.data || null;
614
+ if (this._suspendDataCache) {
615
+ logger.debug('[Cmi5Driver] Loaded suspend_data from State API');
616
+ }
617
+ } catch (error) {
618
+ if (error.response?.status !== 404) {
619
+ logger.warn('[Cmi5Driver] Error fetching suspend_data:', error.message);
620
+ }
621
+ this._suspendDataCache = null;
622
+ }
623
+
624
+ try {
625
+ const response = await xapi.getState({
626
+ agent: params.actor,
627
+ activityId: params.activityId,
628
+ stateId: STATE_ID_BOOKMARK,
629
+ registration: params.registration
630
+ });
631
+ const bookmarkData = response.data;
632
+ if (bookmarkData) {
633
+ this._bookmarkCache = bookmarkData.location || null;
634
+ this._completionStatus = bookmarkData.completionStatus || 'unknown';
635
+ this._successStatus = bookmarkData.successStatus || 'unknown';
636
+ this._score = bookmarkData.score ?? null;
637
+ this._sentComplete = bookmarkData.sentComplete || false;
638
+ this._sentResult = bookmarkData.sentResult || false;
639
+ logger.debug('[Cmi5Driver] Loaded bookmark from State API:', this._bookmarkCache);
640
+ }
641
+ } catch (error) {
642
+ if (error.response?.status !== 404) {
643
+ logger.warn('[Cmi5Driver] Error fetching bookmark:', error.message);
644
+ }
645
+ }
646
+ }
647
+
648
+ async _persistState() {
649
+ const params = this._cmi5.getLaunchParameters();
650
+ const xapi = this._cmi5.xapi;
651
+
652
+ if (this._suspendDataDirty && this._suspendDataCache !== null) {
653
+ try {
654
+ await xapi.setState({
655
+ agent: params.actor,
656
+ activityId: params.activityId,
657
+ stateId: STATE_ID_SUSPEND_DATA,
658
+ registration: params.registration,
659
+ state: this._suspendDataCache
660
+ });
661
+ this._suspendDataDirty = false;
662
+ logger.debug('[Cmi5Driver] Persisted suspend_data');
663
+ } catch (error) {
664
+ logger.error('[Cmi5Driver] Failed to persist suspend_data:', error);
665
+ throw error;
666
+ }
667
+ }
668
+
669
+ if (this._bookmarkDirty) {
670
+ await this._persistBookmark();
671
+ this._bookmarkDirty = false;
672
+ }
673
+ }
674
+
675
+ async _persistBookmark() {
676
+ const params = this._cmi5.getLaunchParameters();
677
+ const xapi = this._cmi5.xapi;
678
+
679
+ try {
680
+ await xapi.setState({
681
+ agent: params.actor,
682
+ activityId: params.activityId,
683
+ stateId: STATE_ID_BOOKMARK,
684
+ registration: params.registration,
685
+ state: {
686
+ location: this._bookmarkCache,
687
+ completionStatus: this._completionStatus,
688
+ successStatus: this._successStatus,
689
+ score: this._score,
690
+ sentComplete: this._sentComplete,
691
+ sentResult: this._sentResult
692
+ }
693
+ });
694
+ logger.debug('[Cmi5Driver] Persisted bookmark');
695
+ } catch (error) {
696
+ logger.error('[Cmi5Driver] Failed to persist bookmark:', error);
697
+ throw error;
698
+ }
699
+ }
700
+
701
+ async _persistSentFlags() {
702
+ this._bookmarkDirty = true;
703
+ await this._persistBookmark();
704
+ }
705
+
706
+ // =========================================================================
707
+ // Private: Development Mode (no real LRS)
708
+ // =========================================================================
709
+
710
+ _loadMockState() {
711
+ try {
712
+ if (this._devApi) {
713
+ const state = this._devApi.getState('cmi5_state');
714
+ if (state) {
715
+ this._mockState = state;
716
+ this._bookmarkCache = state.bookmark || null;
717
+ this._completionStatus = state.completionStatus || 'unknown';
718
+ this._successStatus = state.successStatus || 'unknown';
719
+ this._score = state.score ?? null;
720
+ this._sentComplete = state.sentComplete || false;
721
+ this._sentResult = state.sentResult || false;
722
+ }
723
+ this._mockState.suspendData = this._devApi.getState('suspend_data') || null;
724
+ return;
725
+ }
726
+
727
+ const stored = localStorage.getItem('cmi5_dev_state');
728
+ if (stored) {
729
+ const parsed = JSON.parse(stored);
730
+ this._mockState = parsed;
731
+ this._bookmarkCache = parsed.bookmark || null;
732
+ this._completionStatus = parsed.completionStatus || 'unknown';
733
+ this._successStatus = parsed.successStatus || 'unknown';
734
+ this._score = parsed.score ?? null;
735
+ this._sentComplete = parsed.sentComplete || false;
736
+ this._sentResult = parsed.sentResult || false;
737
+ }
738
+ } catch (_e) {
739
+ this._mockState = {};
740
+ }
741
+ }
742
+
743
+ _saveMockState() {
744
+ try {
745
+ const state = {
746
+ ...this._mockState,
747
+ bookmark: this._bookmarkCache,
748
+ completionStatus: this._completionStatus,
749
+ successStatus: this._successStatus,
750
+ score: this._score,
751
+ sentComplete: this._sentComplete,
752
+ sentResult: this._sentResult
753
+ };
754
+
755
+ if (this._devApi) {
756
+ this._devApi.setState('cmi5_state', state);
757
+ if (this._mockState.suspendData) {
758
+ this._devApi.setState('suspend_data', this._mockState.suspendData);
759
+ }
760
+ return;
761
+ }
762
+
763
+ localStorage.setItem('cmi5_dev_state', JSON.stringify(state));
764
+ } catch (e) {
765
+ logger.warn('[Cmi5Driver] Failed to save mock state:', e);
766
+ }
767
+ }
768
+ }