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,423 @@
1
+ /**
2
+ * @file objective-manager.js
3
+ * @description Manages learning objectives. It is the single source of truth for
4
+ * all objective-related data and persists its state through the StateManager.
5
+ */
6
+
7
+ import { deepClone } from '../utilities/utilities.js';
8
+ import { logger } from '../utilities/logger.js';
9
+ import stateManager from '../state/index.js';
10
+ import { eventBus } from '../core/event-bus.js';
11
+
12
+ class ObjectiveManager {
13
+ constructor() {
14
+ this.objectives = {};
15
+ this.isInitialized = false;
16
+ this.earlyQueue = []; // Queue for calls made before initialization
17
+ this.DOMAIN_KEY = 'objectives';
18
+ this.SOURCE = 'objective-manager';
19
+ this.criteriaConfig = []; // Store objectives with criteria
20
+ }
21
+
22
+ /**
23
+ * Initializes the manager by loading state from the StateManager and setting up objectives from config.
24
+ * @param {Array<object>} [objectivesConfig] - Optional array of objective configurations from course-config.js
25
+ * @throws {Error} If already initialized
26
+ */
27
+ initialize(objectivesConfig = []) {
28
+ if (this.isInitialized) {
29
+ throw new Error('ObjectiveManager: Already initialized. Do not call initialize() more than once.');
30
+ }
31
+
32
+ // Build set of configured objective IDs for validation
33
+ const configuredIds = new Set(objectivesConfig.map(obj => obj.id));
34
+
35
+ // Load from stateManager domain (transparently routed to CMI by stateManager)
36
+ const storedObjectives = stateManager.getDomainState(this.DOMAIN_KEY);
37
+ if (storedObjectives && typeof storedObjectives === 'object') {
38
+ // Validate stored objectives against current config
39
+ const storedIds = Object.keys(storedObjectives);
40
+ const orphanedIds = storedIds.filter(id => !configuredIds.has(id));
41
+
42
+ if (orphanedIds.length > 0 && objectivesConfig.length > 0) {
43
+ // Found objectives in storage that aren't in current config
44
+ const message = `Found ${orphanedIds.length} stored objective(s) not in current config: ${orphanedIds.join(', ')}. ` +
45
+ 'These may be from a previous version of the course.';
46
+
47
+ if (import.meta.env.DEV) {
48
+ // Dev mode: Warn about orphaned objectives (could indicate config issue)
49
+ logger.warn(`[ObjectiveManager] ${message}`);
50
+ }
51
+ // In both modes, we keep orphaned objectives in CMI (can't remove from LMS)
52
+ // but they won't affect course logic since they're not in config
53
+ }
54
+
55
+ this.objectives = storedObjectives;
56
+ }
57
+
58
+ // Mark the manager initialized before seeding new objectives so SCORM syncs work immediately
59
+ this.isInitialized = true;
60
+
61
+ // Initialize objectives from config if provided (only if they don't exist)
62
+ if (objectivesConfig.length > 0) {
63
+ objectivesConfig.forEach(objective => {
64
+ if (!this.objectives[objective.id]) {
65
+ // OPTIMIZATION: Do NOT store description - it's in course-config.js
66
+ this.setObjective({
67
+ id: objective.id,
68
+ completion_status: objective.initialCompletion ?? 'incomplete',
69
+ success_status: objective.initialSuccess ?? 'unknown',
70
+ score: objective.initialScore ?? null
71
+ });
72
+ }
73
+ });
74
+ logger.debug(`[ObjectiveManager] Initialized ${objectivesConfig.length} objective(s) from configuration.`);
75
+
76
+ // Enable automatic criteria tracking
77
+ this.enableCriteriaTracking(objectivesConfig);
78
+ }
79
+ logger.debug('[ObjectiveManager] Initialized with objectives:', this.objectives);
80
+
81
+ // Process any calls that were queued before initialization was complete
82
+ this._processEarlyQueue();
83
+ }
84
+
85
+ /**
86
+ * Processes and executes method calls that were queued before the manager was initialized.
87
+ * @private
88
+ */
89
+ _processEarlyQueue() {
90
+ if (this.earlyQueue.length > 0) {
91
+ logger.debug(`[ObjectiveManager] Processing ${this.earlyQueue.length} early-queued call(s).`);
92
+ this.earlyQueue.forEach(({ method, args }) => {
93
+ if (typeof this[method] === 'function') {
94
+ this[method](...args);
95
+ }
96
+ });
97
+ this.earlyQueue = []; // Clear the queue
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Retrieves all objectives.
103
+ * @returns {Array<object>} A deep-cloned array of all objective objects.
104
+ */
105
+ getObjectives() {
106
+ return deepClone(Object.values(this.objectives));
107
+ }
108
+
109
+ /**
110
+ * Retrieves a single objective by its ID.
111
+ * @param {string} id - The unique identifier for the objective.
112
+ * @returns {object|null} A deep-cloned objective object or null if not found.
113
+ */
114
+ getObjective(id) {
115
+ if (!id || !this.objectives[id]) {
116
+ return null;
117
+ }
118
+ return deepClone(this.objectives[id]);
119
+ }
120
+
121
+ /**
122
+ * Creates or updates an objective.
123
+ * @param {object} objectiveData - The data for the objective.
124
+ * @param {string} objectiveData.id - The unique identifier for the objective.
125
+ * @param {string} [objectiveData.success_status] - The success status ('passed', 'failed', 'unknown').
126
+ * @param {string} [objectiveData.completion_status] - The completion status ('completed', 'incomplete').
127
+ * @param {number} [objectiveData.score] - The score (0-100).
128
+ * @param {string} [objectiveData.description] - A description of the objective.
129
+ * @returns {object|undefined} The updated objective object, or undefined if queued before initialization.
130
+ * @throws {Error} If objectiveData is missing or id is not provided
131
+ */
132
+ setObjective(objectiveData) {
133
+ if (!this.isInitialized) {
134
+ this.earlyQueue.push({ method: 'setObjective', args: [objectiveData] });
135
+ return;
136
+ }
137
+
138
+ const { id } = objectiveData;
139
+ if (!id) {
140
+ throw new Error('ObjectiveManager: setObjective requires an id.');
141
+ }
142
+
143
+ const existing = this.objectives[id] || { id };
144
+ const updated = { ...existing, ...objectiveData };
145
+
146
+ this.objectives[id] = updated;
147
+
148
+ // Persist via stateManager domain (transparently routed to CMI by stateManager)
149
+ stateManager.setDomainState(this.DOMAIN_KEY, this.objectives, { source: this.SOURCE });
150
+
151
+ eventBus.emit('objective:updated', updated);
152
+
153
+ // Emit specific score event if score was updated (for ScoreManager)
154
+ if (typeof updated.score === 'number') {
155
+ eventBus.emit('objective:score:updated', {
156
+ objectiveId: id,
157
+ score: updated.score
158
+ });
159
+ }
160
+
161
+ return deepClone(updated);
162
+ }
163
+
164
+ /**
165
+ * Valid success status values per SCORM spec.
166
+ * @private
167
+ */
168
+ static VALID_SUCCESS_STATUSES = ['passed', 'failed', 'unknown'];
169
+
170
+ /**
171
+ * Valid completion status values per SCORM spec.
172
+ * @private
173
+ */
174
+ static VALID_COMPLETION_STATUSES = ['completed', 'incomplete'];
175
+
176
+ /**
177
+ * A helper method to specifically update the success status of an objective.
178
+ * @param {string} id - The objective ID.
179
+ * @param {string} success_status - The success status ('passed', 'failed', 'unknown').
180
+ * @param {number|null} [score] - An optional score to set along with the status.
181
+ * @throws {Error} If objective with given ID is not found
182
+ * @throws {Error} If success_status is not a valid value
183
+ */
184
+ setSuccessStatus(id, success_status, score = null) {
185
+ if (!this.isInitialized) {
186
+ this.earlyQueue.push({ method: 'setSuccessStatus', args: [id, success_status, score] });
187
+ return;
188
+ }
189
+ if (!ObjectiveManager.VALID_SUCCESS_STATUSES.includes(success_status)) {
190
+ throw new Error(`ObjectiveManager: Invalid success_status "${success_status}". Must be one of: ${ObjectiveManager.VALID_SUCCESS_STATUSES.join(', ')}`);
191
+ }
192
+ const objective = this.getObjective(id);
193
+ if (!objective) {
194
+ throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
195
+ }
196
+ objective.success_status = success_status;
197
+ this.setObjective(objective);
198
+ if (score !== null) {
199
+ // Validate score through setScore's guard (which will persist separately)
200
+ this.setScore(id, score);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * A helper method to specifically update the completion status of an objective.
206
+ * @param {string} id - The objective ID.
207
+ * @param {string} completion_status - The completion status ('completed', 'incomplete').
208
+ * @throws {Error} If objective with given ID is not found
209
+ * @throws {Error} If completion_status is not a valid value
210
+ */
211
+ setCompletionStatus(id, completion_status) {
212
+ if (!this.isInitialized) {
213
+ this.earlyQueue.push({ method: 'setCompletionStatus', args: [id, completion_status] });
214
+ return;
215
+ }
216
+ if (!ObjectiveManager.VALID_COMPLETION_STATUSES.includes(completion_status)) {
217
+ throw new Error(`ObjectiveManager: Invalid completion_status "${completion_status}". Must be one of: ${ObjectiveManager.VALID_COMPLETION_STATUSES.join(', ')}`);
218
+ }
219
+ const objective = this.getObjective(id);
220
+ if (!objective) {
221
+ throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
222
+ }
223
+ objective.completion_status = completion_status;
224
+ this.setObjective(objective);
225
+ }
226
+
227
+ /**
228
+ * A helper method to specifically update the score of an objective.
229
+ * @param {string} id - The objective ID.
230
+ * @param {number} score - The score (0-100).
231
+ * @throws {Error} If objective with given ID is not found
232
+ * @throws {Error} If score is not a number or out of range
233
+ */
234
+ setScore(id, score) {
235
+ if (!this.isInitialized) {
236
+ this.earlyQueue.push({ method: 'setScore', args: [id, score] });
237
+ return;
238
+ }
239
+ const objective = this.getObjective(id);
240
+ if (!objective) {
241
+ throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
242
+ }
243
+ if (typeof score !== 'number' || isNaN(score) || score < 0 || score > 100) {
244
+ throw new Error(`ObjectiveManager: Score must be a number between 0 and 100, got ${score}`);
245
+ }
246
+ objective.score = score;
247
+ this.setObjective(objective);
248
+ }
249
+
250
+ /**
251
+ * Enables automatic criteria tracking for objectives with built-in criteria.
252
+ * @param {Array<object>} objectivesConfig - Array of objective configurations from course-config.js
253
+ */
254
+ enableCriteriaTracking(objectivesConfig = []) {
255
+ this.criteriaConfig = objectivesConfig.filter(obj => obj.criteria);
256
+
257
+ if (this.criteriaConfig.length === 0) {
258
+ logger.debug('[ObjectiveManager] No objectives with built-in criteria found.');
259
+ return;
260
+ }
261
+
262
+ // Guard against duplicate listener registration
263
+ if (this._criteriaTrackingEnabled) {
264
+ logger.debug('[ObjectiveManager] Criteria tracking already enabled, updating config only.');
265
+ return;
266
+ }
267
+ this._criteriaTrackingEnabled = true;
268
+
269
+ // Listen for view changes to track slide visits
270
+ eventBus.on('view:change', ({ view }) => {
271
+ this._handleSlideVisit(view);
272
+ this._checkTimeBasedObjectives(view);
273
+ });
274
+
275
+ // Listen for flag changes to track flag-based objectives
276
+ eventBus.on('flag:updated', ({ key, value }) => {
277
+ this._checkFlagBasedObjectives(key, value);
278
+ });
279
+
280
+ // Also listen for flag removals — a removed flag is effectively undefined/falsy
281
+ eventBus.on('flag:removed', ({ key }) => {
282
+ this._checkFlagBasedObjectives(key, undefined);
283
+ });
284
+
285
+ logger.debug(`[ObjectiveManager] Criteria tracking enabled for ${this.criteriaConfig.length} objective(s).`);
286
+ }
287
+
288
+ /**
289
+ * Handles slide visit event and checks objectives with visit-based criteria.
290
+ * @private
291
+ * @param {string} slideId - The ID of the visited slide
292
+ */
293
+ _handleSlideVisit(slideId) {
294
+ // Get visited slides from state manager
295
+ const navigationState = stateManager.getDomainState('navigation');
296
+ const visitedSlides = navigationState?.visitedSlides || [];
297
+
298
+ // Create a new set of visited slides including the current one for this check
299
+ const allVisitedSlides = new Set([...visitedSlides, slideId]);
300
+
301
+ this.criteriaConfig.forEach(objective => {
302
+ // Skip if already completed
303
+ const current = this.objectives[objective.id];
304
+ if (current && current.completion_status === 'completed') {
305
+ return;
306
+ }
307
+
308
+ const { criteria } = objective;
309
+
310
+ switch (criteria.type) {
311
+ case 'slideVisited': {
312
+ if (criteria.slideId === slideId) {
313
+ this.setCompletionStatus(objective.id, 'completed');
314
+ logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: slideVisited`);
315
+ }
316
+ break;
317
+ }
318
+
319
+ case 'allSlidesVisited': {
320
+ const requiredSlides = criteria.slideIds || [];
321
+ const allVisited = requiredSlides.every(sid => allVisitedSlides.has(sid));
322
+ if (allVisited) {
323
+ this.setCompletionStatus(objective.id, 'completed');
324
+ logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: allSlidesVisited`);
325
+ }
326
+ break;
327
+ }
328
+ }
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Checks if any time-based objectives are met for a given slide.
334
+ * Uses centralized timing data from AppActions (stored in sessionData domain).
335
+ * @private
336
+ * @param {string} slideId - The ID of the slide
337
+ */
338
+ _checkTimeBasedObjectives(slideId) {
339
+ // Get slide durations from centralized session data
340
+ const sessionData = stateManager.getDomainState('sessionData');
341
+ const slideDurations = sessionData?.slideDurations || {};
342
+
343
+ this.criteriaConfig.forEach(objective => {
344
+ // Skip if already completed
345
+ const current = this.objectives[objective.id];
346
+ if (current && current.completion_status === 'completed') {
347
+ return;
348
+ }
349
+
350
+ const { criteria } = objective;
351
+
352
+ if (criteria.type === 'timeOnSlide' && criteria.slideId === slideId) {
353
+ const totalMilliseconds = slideDurations[slideId] || 0;
354
+ const totalSeconds = totalMilliseconds / 1000;
355
+
356
+ if (totalSeconds >= (criteria.minSeconds || 0)) {
357
+ this.setCompletionStatus(objective.id, 'completed');
358
+ logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: timeOnSlide (${totalSeconds.toFixed(1)}s)`);
359
+ }
360
+ }
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Checks if any flag-based objectives are met.
366
+ * @private
367
+ * @param {string} flagKey - The key of the flag that was updated
368
+ * @param {any} flagValue - The new value of the flag
369
+ */
370
+ _checkFlagBasedObjectives(flagKey, flagValue) {
371
+ // Get all current flags
372
+ const flags = stateManager.getDomainState('flags') || {};
373
+
374
+ this.criteriaConfig.forEach(objective => {
375
+ // Skip if already completed
376
+ const current = this.objectives[objective.id];
377
+ if (current && current.completion_status === 'completed') {
378
+ return;
379
+ }
380
+
381
+ const { criteria } = objective;
382
+
383
+ if (criteria.type === 'flag') {
384
+ // Single flag check
385
+ if (criteria.key === flagKey) {
386
+ let isMet = false;
387
+
388
+ if (criteria.equals !== undefined) {
389
+ isMet = flagValue === criteria.equals;
390
+ } else {
391
+ // Default: check if flag is truthy
392
+ isMet = !!flagValue;
393
+ }
394
+
395
+ if (isMet) {
396
+ this.setCompletionStatus(objective.id, 'completed');
397
+ logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: flag "${flagKey}" = ${flagValue}`);
398
+ }
399
+ }
400
+ } else if (criteria.type === 'allFlags') {
401
+ // Multiple flags check - all must be truthy (or match equals values)
402
+ const requiredFlags = criteria.flags || [];
403
+ const allMet = requiredFlags.every(flagConfig => {
404
+ const key = typeof flagConfig === 'string' ? flagConfig : flagConfig.key;
405
+ const value = flags[key];
406
+
407
+ if (typeof flagConfig === 'object' && flagConfig.equals !== undefined) {
408
+ return value === flagConfig.equals;
409
+ }
410
+ return !!value;
411
+ });
412
+
413
+ if (allMet) {
414
+ this.setCompletionStatus(objective.id, 'completed');
415
+ logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: allFlags`);
416
+ }
417
+ }
418
+ });
419
+ }
420
+ }
421
+
422
+ const instance = new ObjectiveManager();
423
+ export default instance;