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,441 @@
1
+ /**
2
+ * @file score-manager.js
3
+ * @description Manages top-level course score (cmi.score.raw/scaled/min/max) based on configurable formulas.
4
+ *
5
+ * ARCHITECTURE:
6
+ * - Singleton manager (like ObjectiveManager)
7
+ * - Initialized once with course scoring config from course-config.js
8
+ * - Listens to assessment completion and objective score updates
9
+ * - Calculates and reports cmi.score.raw based on configured formula
10
+ * - Supports: average, weighted average, minimum, maximum, custom functions
11
+ *
12
+ * CONFIGURATION:
13
+ * USAGE (by course authors in course-config.js):
14
+ * scoring: {
15
+ * type: 'average',
16
+ * sources: ['assessment:final-exam', 'assessment:midterm']
17
+ * }
18
+ *
19
+ * Or weighted:
20
+ * scoring: {
21
+ * type: 'weighted',
22
+ * sources: [
23
+ * { id: 'assessment:final-exam', weight: 0.6 },
24
+ * { id: 'objective:practical-mastery', weight: 0.4 }
25
+ * ]
26
+ * }
27
+ *
28
+ * Or custom:
29
+ * scoring: {
30
+ * type: 'custom',
31
+ * calculate: (scores) => {
32
+ * const exam = scores['assessment:final-exam'];
33
+ * const midterm = scores['assessment:midterm'];
34
+ * return exam && midterm ? Math.max(exam, midterm) : null;
35
+ * }
36
+ * }
37
+ */
38
+
39
+ import { logger } from '../utilities/logger.js';
40
+ import { eventBus } from '../core/event-bus.js';
41
+ import stateManager from '../state/index.js';
42
+ import objectiveManager from './objective-manager.js';
43
+
44
+ class ScoreManager {
45
+ constructor() {
46
+ this.isInitialized = false;
47
+ this.config = null;
48
+ this.cachedScores = {}; // Cache of source scores
49
+ }
50
+
51
+ /**
52
+ * Initializes the score manager with course scoring configuration.
53
+ * @param {Object} config - Scoring configuration from course-config.js
54
+ * @param {string} config.type - Scoring formula type: 'average', 'weighted', 'minimum', 'maximum', 'custom', or null to disable
55
+ * @param {Array} config.sources - Array of source IDs (strings) or source objects {id, weight}
56
+ * @param {Function} config.calculate - Custom calculation function (required if type='custom')
57
+ * @throws {Error} If configuration is invalid
58
+ */
59
+ initialize(config) {
60
+ if (this.isInitialized) {
61
+ throw new Error('[ScoreManager] Already initialized. Do not call initialize() more than once.');
62
+ }
63
+
64
+ // If no scoring config or explicitly disabled, skip initialization
65
+ if (!config || config.type === null || config.type === 'none') {
66
+ this.isInitialized = true;
67
+ logger.debug('[ScoreManager] Course scoring disabled. cmi.score.raw will not be set.');
68
+ return;
69
+ }
70
+
71
+ // Validate configuration
72
+ this._validateConfig(config);
73
+ this.config = config;
74
+
75
+ // Subscribe to events
76
+ this._subscribeToEvents();
77
+
78
+ // Load existing scores from state
79
+ this._loadExistingScores();
80
+
81
+ this.isInitialized = true;
82
+ logger.debug('[ScoreManager] Initialized with scoring type:', config.type);
83
+ }
84
+
85
+ /**
86
+ * Validates scoring configuration.
87
+ * @private
88
+ */
89
+ _validateConfig(config) {
90
+ if (!config.type) {
91
+ throw new Error('[ScoreManager] Configuration must include "type" field');
92
+ }
93
+
94
+ const validTypes = ['average', 'weighted', 'minimum', 'maximum', 'custom'];
95
+ if (!validTypes.includes(config.type)) {
96
+ throw new Error(`[ScoreManager] Invalid scoring type "${config.type}". Must be one of: ${validTypes.join(', ')}`);
97
+ }
98
+
99
+ if (!config.sources || !Array.isArray(config.sources) || config.sources.length === 0) {
100
+ throw new Error('[ScoreManager] Configuration must include non-empty "sources" array');
101
+ }
102
+
103
+ // Validate weighted sources
104
+ if (config.type === 'weighted') {
105
+ const hasInvalidSources = config.sources.some(source =>
106
+ typeof source !== 'object' || !source.id || typeof source.weight !== 'number'
107
+ );
108
+ if (hasInvalidSources) {
109
+ throw new Error('[ScoreManager] Weighted scoring requires sources with {id, weight} format');
110
+ }
111
+
112
+ // Validate weights sum - must equal 1.0
113
+ const totalWeight = config.sources.reduce((sum, s) => sum + s.weight, 0);
114
+ if (Math.abs(totalWeight - 1.0) > 0.001) {
115
+ throw new Error(`[ScoreManager] Weights sum to ${totalWeight}, not 1.0. Weights must sum exactly to 1.0. Current weights: ${JSON.stringify(config.sources.map(s => ({id: s.id, weight: s.weight})))}`);
116
+ }
117
+ }
118
+
119
+ // Validate custom function
120
+ if (config.type === 'custom') {
121
+ if (typeof config.calculate !== 'function') {
122
+ throw new Error('[ScoreManager] Custom scoring type requires "calculate" function');
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Subscribes to assessment and objective events.
129
+ * @private
130
+ */
131
+ _subscribeToEvents() {
132
+ // Listen for assessment completions (emitted after onComplete callback)
133
+ eventBus.on('assessment:submitted', (data) => {
134
+ const { assessmentId, results } = data;
135
+ if (results && typeof results.scorePercentage === 'number') {
136
+ this._updateSourceScore(`assessment:${assessmentId}`, results.scorePercentage);
137
+ }
138
+ });
139
+
140
+ // Listen for objective score updates
141
+ eventBus.on('objective:score:updated', (data) => {
142
+ const { objectiveId, score } = data;
143
+ if (typeof score === 'number') {
144
+ this._updateSourceScore(`objective:${objectiveId}`, score);
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Loads existing scores from state (objectives and assessments).
151
+ * @private
152
+ */
153
+ _loadExistingScores() {
154
+ if (!this.config) return;
155
+
156
+ // Extract source IDs from config
157
+ const sourceIds = this.config.sources.map(source =>
158
+ typeof source === 'string' ? source : source.id
159
+ );
160
+
161
+ sourceIds.forEach(sourceId => {
162
+ const colonIndex = sourceId.indexOf(':');
163
+ const type = sourceId.substring(0, colonIndex);
164
+ const id = sourceId.substring(colonIndex + 1);
165
+
166
+ if (type === 'objective') {
167
+ // Load objective score from ObjectiveManager
168
+ try {
169
+ const objective = objectiveManager.getObjective(id);
170
+ if (objective && typeof objective.score === 'number') {
171
+ this.cachedScores[sourceId] = objective.score;
172
+ }
173
+ } catch (_error) {
174
+ // Objective might not exist yet - that's OK
175
+ }
176
+ } else if (type === 'assessment') {
177
+ // Load assessment score from state
178
+ try {
179
+ const domainKey = `assessment_${id}`;
180
+ const assessmentState = stateManager.getDomainState(domainKey);
181
+ const summary = assessmentState?.summary;
182
+ if (summary && summary.lastResults && typeof summary.lastResults.scorePercentage === 'number') {
183
+ this.cachedScores[sourceId] = summary.lastResults.scorePercentage;
184
+ }
185
+ } catch (_error) {
186
+ // Assessment might not be completed yet - that's OK
187
+ }
188
+ }
189
+ });
190
+
191
+ logger.debug('[ScoreManager] Loaded existing scores:', this.cachedScores);
192
+ }
193
+
194
+ /**
195
+ * Updates a source score and recalculates course score.
196
+ * @private
197
+ * @param {string} sourceId - Source identifier (e.g., 'assessment:final-exam')
198
+ * @param {number} score - Score value (0-100)
199
+ */
200
+ _updateSourceScore(sourceId, score) {
201
+ if (!this.isInitialized) return;
202
+
203
+ // Check if this source is configured
204
+ const sourceIds = this.config.sources.map(source =>
205
+ typeof source === 'string' ? source : source.id
206
+ );
207
+
208
+ if (!sourceIds.includes(sourceId)) {
209
+ return; // Not a configured source, ignore
210
+ }
211
+
212
+ // Update cache
213
+ const oldScore = this.cachedScores[sourceId];
214
+ this.cachedScores[sourceId] = score;
215
+
216
+ logger.debug(`[ScoreManager] Score updated: ${sourceId} = ${score} (was ${oldScore || 'none'})`);
217
+
218
+ // Recalculate and report course score
219
+ this._calculateAndReportScore();
220
+ }
221
+
222
+ /**
223
+ * Calculates course score based on configured formula and reports to SCORM.
224
+ * @private
225
+ */
226
+ _calculateAndReportScore() {
227
+ if (!this.isInitialized || !this.config) return;
228
+
229
+ const calculatedScore = this._calculateScore();
230
+
231
+ if (calculatedScore === null) {
232
+ logger.debug('[ScoreManager] Insufficient data to calculate course score');
233
+ return;
234
+ }
235
+
236
+ // Validate score range
237
+ if (calculatedScore < 0 || calculatedScore > 100 || isNaN(calculatedScore)) {
238
+ throw new Error(`[ScoreManager] Calculated score ${calculatedScore} is out of range [0-100]. This indicates a bug in the scoring calculation.`);
239
+ }
240
+
241
+ // Report to SCORM
242
+ try {
243
+ const rawScore = Math.round(calculatedScore * 100) / 100; // Round to 2 decimals
244
+ const scaledScore = rawScore / 100;
245
+
246
+ stateManager.reportScore({
247
+ raw: rawScore,
248
+ scaled: scaledScore,
249
+ min: 0,
250
+ max: 100
251
+ });
252
+
253
+ logger.debug(`[ScoreManager] Course score updated: ${rawScore}% (scaled: ${scaledScore})`);
254
+
255
+ // Emit event for other systems to react
256
+ eventBus.emit('course:score:updated', {
257
+ raw: rawScore,
258
+ scaled: scaledScore,
259
+ sources: { ...this.cachedScores }
260
+ });
261
+
262
+ // Flush immediately — scores are critical data that must survive browser close
263
+ stateManager.flush();
264
+ } catch (error) {
265
+ logger.error(`[ScoreManager] Failed to report score to SCORM: ${error.message}`, { domain: 'score', operation: 'reportScore', stack: error.stack, calculatedScore });
266
+ throw error;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Calculates score based on configured formula.
272
+ * @private
273
+ * @returns {number|null} Calculated score (0-100) or null if insufficient data
274
+ */
275
+ _calculateScore() {
276
+ const { type, sources } = this.config;
277
+
278
+ // Extract scores from cache
279
+ const scores = {};
280
+ const sourceIds = sources.map(source => typeof source === 'string' ? source : source.id);
281
+
282
+ sourceIds.forEach(sourceId => {
283
+ if (this.cachedScores[sourceId] !== undefined) {
284
+ scores[sourceId] = this.cachedScores[sourceId];
285
+ }
286
+ });
287
+
288
+ // Check if we have any scores
289
+ const availableCount = Object.keys(scores).length;
290
+
291
+ if (availableCount === 0) {
292
+ return null; // No scores available yet
293
+ }
294
+
295
+ switch (type) {
296
+ case 'average':
297
+ return this._calculateAverage(scores);
298
+
299
+ case 'weighted':
300
+ return this._calculateWeighted(scores, sources);
301
+
302
+ case 'minimum':
303
+ return this._calculateMinimum(scores);
304
+
305
+ case 'maximum':
306
+ return this._calculateMaximum(scores);
307
+
308
+ case 'custom':
309
+ return this._calculateCustom(scores);
310
+
311
+ default:
312
+ throw new Error(`[ScoreManager] Unknown scoring type: ${type}. Valid types: average, weighted, custom.`);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Calculates simple average of available scores.
318
+ * @private
319
+ */
320
+ _calculateAverage(scores) {
321
+ const values = Object.values(scores);
322
+ if (values.length === 0) return null;
323
+
324
+ const sum = values.reduce((acc, val) => acc + val, 0);
325
+ return sum / values.length;
326
+ }
327
+
328
+ /**
329
+ * Calculates weighted average, normalizing weights for available scores.
330
+ * @private
331
+ */
332
+ _calculateWeighted(scores, sources) {
333
+ let weightedSum = 0;
334
+ let totalWeight = 0;
335
+
336
+ sources.forEach(source => {
337
+ const score = scores[source.id];
338
+ if (score !== undefined) {
339
+ weightedSum += score * source.weight;
340
+ totalWeight += source.weight;
341
+ }
342
+ });
343
+
344
+ if (totalWeight === 0) return null;
345
+
346
+ // Normalize by actual total weight (handles partial completion)
347
+ return weightedSum / totalWeight;
348
+ }
349
+
350
+ /**
351
+ * Returns minimum of available scores.
352
+ * @private
353
+ */
354
+ _calculateMinimum(scores) {
355
+ const values = Object.values(scores);
356
+ if (values.length === 0) return null;
357
+
358
+ return Math.min(...values);
359
+ }
360
+
361
+ /**
362
+ * Returns maximum of available scores.
363
+ * @private
364
+ */
365
+ _calculateMaximum(scores) {
366
+ const values = Object.values(scores);
367
+ if (values.length === 0) return null;
368
+
369
+ return Math.max(...values);
370
+ }
371
+
372
+ /**
373
+ * Executes custom calculation function.
374
+ * @private
375
+ */
376
+ _calculateCustom(scores) {
377
+ try {
378
+ const result = this.config.calculate(scores);
379
+
380
+ if (result === null || result === undefined) {
381
+ return null;
382
+ }
383
+
384
+ if (typeof result !== 'number' || isNaN(result)) {
385
+ throw new Error(`[ScoreManager] Custom calculate function must return a number or null. Got: ${typeof result}`);
386
+ }
387
+
388
+ return result;
389
+ } catch (error) {
390
+ throw new Error(`[ScoreManager] Error in custom calculate function: ${error.message}`);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Manually triggers score recalculation (for testing or edge cases).
396
+ * @public
397
+ */
398
+ recalculate() {
399
+ if (!this.isInitialized) {
400
+ throw new Error('[ScoreManager] Cannot recalculate: not initialized. Call initialize() first.');
401
+ }
402
+
403
+ this._calculateAndReportScore();
404
+ }
405
+
406
+ /**
407
+ * Gets current course score without recalculating.
408
+ * @public
409
+ * @returns {Object|null} {raw, scaled, sources} or null if not available
410
+ */
411
+ getCurrentScore() {
412
+ if (!this.isInitialized) {
413
+ return null;
414
+ }
415
+
416
+ const calculatedScore = this._calculateScore();
417
+ if (calculatedScore === null) {
418
+ return null;
419
+ }
420
+
421
+ const rawScore = Math.round(calculatedScore * 100) / 100;
422
+ return {
423
+ raw: rawScore,
424
+ scaled: rawScore / 100,
425
+ sources: { ...this.cachedScores }
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Gets all cached source scores.
431
+ * @public
432
+ * @returns {Object} Map of sourceId => score
433
+ */
434
+ getSourceScores() {
435
+ return { ...this.cachedScores };
436
+ }
437
+ }
438
+
439
+ // Export singleton instance
440
+ const scoreManager = new ScoreManager();
441
+ export default scoreManager;