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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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;
|