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,471 @@
1
+ /**
2
+ * @file AssessmentFactory.js
3
+ * @description Internal factory for creating assessment instances.
4
+ * Encapsulates the State-UI-Actions wiring pattern.
5
+ *
6
+ * This file should NOT be imported directly by course authors.
7
+ * Use the public API in assessment-manager.js instead.
8
+ */
9
+
10
+ import { eventBus } from '../core/event-bus.js';
11
+ import stateManager from '../state/index.js';
12
+ import { createAssessmentState } from './AssessmentState.js';
13
+ import { createAssessmentUI } from './AssessmentUI.js';
14
+ import { createAssessmentActions } from './AssessmentActions.js';
15
+ import { shuffleArray } from '../utilities/utilities.js';
16
+ import * as AppUI from '../app/AppUI.js';
17
+ import { getVisitedSlides } from '../navigation/NavigationState.js';
18
+
19
+ // Import from the central interaction type catalog
20
+ // This provides unified access to built-in and custom interaction types
21
+ import { getCreator, getMetadata, isRegistered } from '../core/interaction-catalog.js';
22
+ import { logger } from '../utilities/logger.js';
23
+
24
+ /**
25
+ * Get interaction type info from the registry.
26
+ * Returns { creator, metadata } or null if type is not registered.
27
+ * @param {string} type - Interaction type name
28
+ * @returns {{ creator: function, metadata: object }|null}
29
+ */
30
+ function getInteractionTypeInfo(type) {
31
+ if (!isRegistered(type)) {
32
+ return null;
33
+ }
34
+ return {
35
+ creator: getCreator(type),
36
+ metadata: getMetadata(type)
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Selects questions from question banks according to configuration.
42
+ * @private
43
+ * @param {Array} questionBanks - Array of bank configurations
44
+ * @returns {Array} Selected questions with metadata
45
+ */
46
+ function _selectQuestionsFromBanks(questionBanks) {
47
+ const selectedQuestions = [];
48
+
49
+ questionBanks.forEach(bank => {
50
+ const { id: bankId, questions, selectCount } = bank;
51
+
52
+ // Handle 'all' case - select all questions without randomization
53
+ if (selectCount === 'all') {
54
+ questions.forEach((q, idx) => {
55
+ selectedQuestions.push({
56
+ ...q,
57
+ _meta: {
58
+ bankId,
59
+ originalIndex: idx
60
+ }
61
+ });
62
+ });
63
+ return;
64
+ }
65
+
66
+ // Randomize and select N questions
67
+ const shuffled = shuffleArray(questions);
68
+ const selected = shuffled.slice(0, selectCount);
69
+
70
+ selected.forEach((q, _idx) => {
71
+ const originalIndex = questions.indexOf(q);
72
+ selectedQuestions.push({
73
+ ...q,
74
+ _meta: {
75
+ bankId,
76
+ originalIndex
77
+ }
78
+ });
79
+ });
80
+ });
81
+
82
+ return selectedQuestions;
83
+ }
84
+
85
+ /**
86
+ * Required interface for all question instances.
87
+ * This documents the contract that interaction components must fulfill.
88
+ */
89
+ const REQUIRED_METHODS = {
90
+ 'render': 'function', // render(container) - Renders question to DOM
91
+ 'getResponse': 'function', // getResponse() - Returns current user response
92
+ 'setResponse': 'function', // setResponse(value) - Sets response programmatically
93
+ 'evaluate': 'function', // evaluate(response) - Returns {correct: boolean}
94
+ };
95
+
96
+ /**
97
+ * Optional methods that interaction components may implement:
98
+ * - reset(): function - Clears user input
99
+ * - checkAnswer(): function - Shows immediate feedback
100
+ * - showHint(): function - Displays hint to user
101
+ * - getCorrectAnswer(): function - Returns the correct answer for automation/review
102
+ */
103
+
104
+ /**
105
+ * Validates that a question instance implements the required interface.
106
+ * @private
107
+ * @throws {Error} If any required method is missing
108
+ */
109
+ function _validateQuestionInstance(instance, questionConfig, assessmentId) {
110
+ const errors = [];
111
+
112
+ for (const [methodName, expectedType] of Object.entries(REQUIRED_METHODS)) {
113
+ if (typeof instance[methodName] !== expectedType) {
114
+ errors.push(`Missing required method '${methodName}' (expected ${expectedType}, got ${typeof instance[methodName]})`);
115
+ }
116
+ }
117
+
118
+ if (errors.length > 0) {
119
+ const errorMessage = `[AssessmentFactory:${assessmentId}] Question type '${questionConfig.type}' (ID: ${questionConfig.id}) has invalid interface:\n - ${errors.join('\n - ')}`;
120
+ logger.error(errorMessage, { domain: 'assessment', operation: 'validateQuestionInstance', assessmentId, questionType: questionConfig.type, questionId: questionConfig.id });
121
+ throw new Error(errorMessage);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Creates a question instance with SCORM persistence methods.
127
+ * Attaches metadata to the instance for type-specific formatting.
128
+ * @private
129
+ */
130
+ function _createQuestionInstance(questionConfig, index, assessmentState, assessmentId) {
131
+ const type = questionConfig.type;
132
+ const typeInfo = getInteractionTypeInfo(type);
133
+
134
+ // This should never happen because validation happens first, but guard anyway
135
+ if (!typeInfo) {
136
+ const errorMessage = `[AssessmentFactory:${assessmentId}] Unknown question type: ${type}. Ensure it is registered in the interaction registry.`;
137
+ logger.error(errorMessage, { domain: 'assessment', operation: 'createQuestionInstance', assessmentId, questionType: type });
138
+ throw new Error(errorMessage);
139
+ }
140
+
141
+ // Create base instance
142
+ // Assessments use controlled mode: no Check Answer buttons, centralized evaluation/SCORM recording
143
+ // For immediate feedback, use standalone interactions in regular slides instead
144
+ const baseInstance = typeInfo.creator({ ...questionConfig, controlled: true });
145
+
146
+ // Validate interface
147
+ _validateQuestionInstance(baseInstance, questionConfig, assessmentId);
148
+
149
+ // Wrap with SCORM persistence layer and attach metadata
150
+ return {
151
+ ...baseInstance,
152
+ metadata: typeInfo.metadata, // Attach metadata for AssessmentUI/Actions to use
153
+ async persistToSCORM() {
154
+ const response = baseInstance.getResponse();
155
+ await assessmentState.saveResponse(index, response);
156
+ },
157
+ restoreFromSCORM() {
158
+ const savedResponse = assessmentState.getResponse(index);
159
+ if (savedResponse !== null && savedResponse !== undefined) {
160
+ baseInstance.setResponse(savedResponse);
161
+ }
162
+ }
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Creates a complete assessment instance with all wiring.
168
+ * Follows the State-UI-Actions pattern.
169
+ *
170
+ * @param {Object} config - Assessment configuration (validated by runtime-linter in dev mode)
171
+ * @returns {Object} Assessment instance with render() method
172
+ * @throws {Error} If critical configuration properties are missing
173
+ */
174
+ export function createAssessmentInstance(config) {
175
+ // Config validation done by runtime-linter in dev mode
176
+ // Runtime: only validate that required properties exist (quick check)
177
+ if (!config.id) {
178
+ const error = new Error('Assessment ID required');
179
+ logger.error(error.message, { domain: 'assessment', operation: 'createAssessmentInstance' });
180
+ throw error;
181
+ }
182
+ if (!config.questions && !config.questionBanks) {
183
+ const error = new Error(`Assessment '${config.id}' needs questions or questionBanks`);
184
+ logger.error(error.message, { domain: 'assessment', operation: 'createAssessmentInstance', assessmentId: config.id });
185
+ throw error;
186
+ }
187
+
188
+ const assessmentId = config.id;
189
+
190
+ // Initialize State layer
191
+ const assessmentState = createAssessmentState(config, stateManager);
192
+
193
+ // Determine active questions (from banks or direct config)
194
+ let activeQuestions = config.questions || [];
195
+ let questionInstances = [];
196
+ let finalConfig = config;
197
+
198
+ /**
199
+ * Resolves which questions to use for this assessment session.
200
+ * Priority:
201
+ * 1. Persisted selected questions (resume in-progress session)
202
+ * 2. Question banks (new session with randomization)
203
+ * 3. Direct questions array (legacy/simple mode)
204
+ *
205
+ * In dev mode, throws if persisted question IDs don't match current config.
206
+ * In prod mode, filters out missing questions and continues gracefully.
207
+ */
208
+ function _resolveActiveQuestions() {
209
+ const savedQuestionIds = assessmentState.getSelectedQuestions();
210
+
211
+ if (savedQuestionIds && Array.isArray(savedQuestionIds)) {
212
+ // OPTIMIZATION FIX: Reconstruct full question objects from saved IDs
213
+ // This is needed because we now only store IDs to save suspend_data space
214
+ const allQuestions = _getAllAvailableQuestions();
215
+ const questionMap = new Map(allQuestions.map(q => [q.id, q]));
216
+
217
+ // Check for missing questions (course structure changed)
218
+ const missingIds = savedQuestionIds.filter(id => !questionMap.has(id));
219
+
220
+ if (missingIds.length > 0) {
221
+ const errorMessage = `Assessment "${assessmentId}" has ${missingIds.length} stored question ID(s) that no longer exist: ${missingIds.join(', ')}. ` +
222
+ 'The assessment questions have changed since the learner started.';
223
+
224
+ if (import.meta.env.DEV) {
225
+ // Dev mode: FAIL FAST to help developers catch stale data issues
226
+ const error = new Error(`[AssessmentFactory] ${errorMessage}`);
227
+ logger.error(errorMessage, {
228
+ domain: 'assessment', operation: 'resolve-questions', stack: error.stack,
229
+ assessmentId, missingIds, storedIds: savedQuestionIds,
230
+ availableIds: Array.from(questionMap.keys())
231
+ });
232
+ throw error;
233
+ } else {
234
+ // Production mode: Log warning and filter out missing questions
235
+ // Also emit event so course can potentially track this
236
+ logger.warn(`[AssessmentFactory] ${errorMessage} Continuing with available questions.`);
237
+ eventBus.emit('state:recovered', {
238
+ domain: 'assessment',
239
+ message: errorMessage,
240
+ context: { assessmentId, missingIds },
241
+ action: 'filtered_missing_questions'
242
+ });
243
+ }
244
+ }
245
+
246
+ return savedQuestionIds
247
+ .map(id => questionMap.get(id))
248
+ .filter(q => q !== undefined); // Filter out any missing questions
249
+ }
250
+
251
+ if (config.questionBanks && Array.isArray(config.questionBanks) && config.questionBanks.length > 0) {
252
+ // New session with banks: select and optionally randomize
253
+ let selected = _selectQuestionsFromBanks(config.questionBanks);
254
+
255
+ if (config.settings?.randomizeQuestions) {
256
+ selected = shuffleArray(selected);
257
+ }
258
+
259
+ return selected;
260
+ }
261
+
262
+ // Direct mode: use questions array as-is
263
+ if (config.settings?.randomizeQuestions && Array.isArray(config.questions)) {
264
+ return shuffleArray(config.questions);
265
+ }
266
+
267
+ return config.questions || [];
268
+ }
269
+
270
+ /**
271
+ * Helper to get all available questions from either banks or direct questions array
272
+ */
273
+ function _getAllAvailableQuestions() {
274
+ if (config.questionBanks && Array.isArray(config.questionBanks)) {
275
+ return config.questionBanks.flatMap(bank => bank.questions || []);
276
+ }
277
+ return config.questions || [];
278
+ }
279
+
280
+ // Create question instances with SCORM wrappers
281
+ function _initializeQuestionInstances(questions) {
282
+ return questions.map((q, i) =>
283
+ _createQuestionInstance(q, i, assessmentState, assessmentId)
284
+ );
285
+ }
286
+
287
+ // Track view manager and actions (initialized on render)
288
+ let viewManager = null;
289
+ let assessmentActions = null;
290
+ let _currentContainer = null;
291
+
292
+ /**
293
+ * Renders the assessment to the target container.
294
+ * @param {HTMLElement} targetContainer - DOM element to render into
295
+ * @param {Object} context - Context from ViewManager (contains fromSlide)
296
+ */
297
+ function render(targetContainer, context = {}) {
298
+ if (!targetContainer) {
299
+ const errorMessage = `[AssessmentFactory:${assessmentId}] targetContainer is required for render()`;
300
+ logger.error(errorMessage, { domain: 'assessment', operation: 'render', assessmentId });
301
+ throw new Error(errorMessage);
302
+ }
303
+
304
+ targetContainer.className = 'assessment-navigator';
305
+ _currentContainer = targetContainer;
306
+
307
+ // Load persisted state
308
+ let summary = assessmentState.getSummary();
309
+ const session = assessmentState.getSession();
310
+
311
+ // Initialize summary if this is first time rendering (fire-and-forget)
312
+ if (!summary) {
313
+ assessmentState.updateSummary({ attempts: 0 }).catch(error => {
314
+ logger.error(`Failed to initialize summary: ${error.message}`, { domain: 'assessment', operation: 'initialize-summary', assessmentId });
315
+ });
316
+ // Use optimistic local value until persist completes
317
+ summary = { attempts: 0 };
318
+ }
319
+
320
+ // Resolve active questions for this session
321
+ activeQuestions = _resolveActiveQuestions();
322
+
323
+ // Persist selection if this is a new bank-based session (fire-and-forget)
324
+ if (config.questionBanks && !session?.selectedQuestions) {
325
+ assessmentState.setSelectedQuestions(activeQuestions).catch(error => {
326
+ logger.error(`Failed to persist selected questions: ${error.message}`, { domain: 'assessment', operation: 'persist-selected-questions', assessmentId });
327
+ });
328
+ }
329
+
330
+ // Create question instances based on active questions
331
+ questionInstances = _initializeQuestionInstances(activeQuestions);
332
+
333
+ // Update config with active questions for UI/Actions layers
334
+ finalConfig = { ...config, questions: activeQuestions };
335
+
336
+ // Initialize UI and Actions with immutable references
337
+ // If questions need to change (retake with randomization), slide creates new assessment instance
338
+ const assessmentUIWithQuestions = createAssessmentUI(finalConfig, assessmentState, questionInstances);
339
+ viewManager = assessmentUIWithQuestions.initialize(targetContainer);
340
+ assessmentActions = createAssessmentActions(
341
+ assessmentState,
342
+ viewManager,
343
+ questionInstances,
344
+ finalConfig,
345
+ assessmentUIWithQuestions // Pass full UI object for modal access
346
+ );
347
+ assessmentActions.initialize(targetContainer);
348
+
349
+ // Check for auto-restart from remedial
350
+ const fromSlide = context.fromSlide;
351
+ const remedialSlideIds = config.settings?.remedialSlideIds || [];
352
+ const isReturningFromRemedial = fromSlide && remedialSlideIds.includes(fromSlide);
353
+
354
+ if (isReturningFromRemedial && summary?.lastResults && !summary.lastResults.passed) {
355
+ // Clear session to force a fresh start (Intro screen)
356
+ // We don't increment attempts here; handleStart does that when user clicks Start
357
+ assessmentState.clearSession().then(() => {
358
+ AppUI.showFooter();
359
+ viewManager.showView('intro');
360
+ });
361
+ return;
362
+ }
363
+
364
+ // Determine initial view and set footer visibility
365
+ const savedResults = summary?.lastResults;
366
+ if (savedResults) {
367
+ // Show completed results (summary only - details not persisted to save space)
368
+ // Prepare display data since we don't have full details anymore
369
+ const displayData = _prepareResumedResultsDisplayData(savedResults, finalConfig);
370
+ AppUI.showFooter();
371
+ viewManager.showView('results', displayData);
372
+ return;
373
+ }
374
+
375
+ if (session && !session.submitted) {
376
+ // Restore in-progress session
377
+ const currentView = session.currentView || 'intro';
378
+ if (currentView === 'question' || currentView === 'review') {
379
+ AppUI.hideFooter();
380
+ } else {
381
+ AppUI.showFooter();
382
+ }
383
+ viewManager.showView(currentView);
384
+ return;
385
+ }
386
+
387
+ // Start fresh - intro view shows footer
388
+ AppUI.showFooter();
389
+ viewManager.showView('intro');
390
+ }
391
+
392
+ /**
393
+ * Prepares display data for results screen when resuming from saved summary.
394
+ * Since we only persist summary stats (not full details), reconstruct what's needed.
395
+ * @param {Object} resultsSummary - Minimal summary saved in suspend_data
396
+ * @param {Object} config - Assessment configuration
397
+ * @returns {Object} Display data for results screen
398
+ */
399
+ function _prepareResumedResultsDisplayData(resultsSummary, config) {
400
+ const summary = assessmentState.getSummary();
401
+ const currentAttempts = summary?.attempts || 0;
402
+ const { attemptsBeforeRemedial, attemptsBeforeRestart, allowRetake, settings: _settings } = config;
403
+
404
+ // Reconstruct minimal results object for display
405
+ // Note: detailed question-by-question review not available on resume (only summary)
406
+ const resultsForDisplay = {
407
+ attemptNumber: resultsSummary.attemptNumber,
408
+ totalQuestions: resultsSummary.totalQuestions,
409
+ correctCount: resultsSummary.correctCount,
410
+ scorePercentage: resultsSummary.scorePercentage,
411
+ passed: resultsSummary.passed,
412
+ details: null // Not available after resume - only summary stats
413
+ };
414
+
415
+ // Determine action button (same logic as in AssessmentActions)
416
+ let actionButton = null;
417
+
418
+ if (attemptsBeforeRestart && currentAttempts >= attemptsBeforeRestart) {
419
+ actionButton = {
420
+ type: 'restart',
421
+ action: 'restart-course',
422
+ label: 'Restart Course',
423
+ message: `You've completed ${currentAttempts} attempt(s). You must restart the course to try again.`,
424
+ messageType: 'error'
425
+ };
426
+ } else {
427
+ // Check remedial logic
428
+ let showRemedial = false;
429
+ if (attemptsBeforeRemedial && currentAttempts >= attemptsBeforeRemedial && !resultsSummary.passed) {
430
+ const remedialSlideIds = config.settings?.remedialSlideIds || (config.settings?.remedialSlideId ? [config.settings.remedialSlideId] : []);
431
+
432
+ // Check if remedial content has already been viewed
433
+ const visitedSlides = getVisitedSlides() || [];
434
+
435
+ const remedialViewed = remedialSlideIds.length > 0 && remedialSlideIds.every(id => visitedSlides.includes(id));
436
+
437
+ if (remedialSlideIds.length > 0 && !remedialViewed) {
438
+ showRemedial = true;
439
+ actionButton = {
440
+ type: 'remedial',
441
+ action: 'go-to-remedial',
442
+ label: 'Review Content',
443
+ message: `Please review the content before attempting again (Attempt ${currentAttempts}/${attemptsBeforeRestart || '∞'}).`,
444
+ messageType: 'warning',
445
+ attemptsMessage: `Attempts: ${currentAttempts}${attemptsBeforeRestart ? `/${attemptsBeforeRestart}` : ''}`
446
+ };
447
+ }
448
+ }
449
+
450
+ // Fallback to standard retake if remedial not shown
451
+ if (!showRemedial && allowRetake !== false && !resultsSummary.passed) {
452
+ actionButton = {
453
+ type: 'retake',
454
+ action: 'retake',
455
+ label: 'Retake Assessment',
456
+ attemptsMessage: `Attempts: ${currentAttempts}${attemptsBeforeRestart ? `/${attemptsBeforeRestart}` : ''}`
457
+ };
458
+ }
459
+ }
460
+
461
+ return {
462
+ ...resultsForDisplay,
463
+ actionButton,
464
+ timeSpent: null // Time data not preserved after session ends
465
+ };
466
+ }
467
+
468
+ return {
469
+ render,
470
+ };
471
+ }