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,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Validation Rules
|
|
3
|
+
*
|
|
4
|
+
* Pure validation logic used by both:
|
|
5
|
+
* - framework/js/dev/runtime-linter.js (browser, uses DOM)
|
|
6
|
+
* - lib/build-linter.js (Node.js, uses source-parser)
|
|
7
|
+
*
|
|
8
|
+
* These functions contain no environment-specific code (no DOM, no fs).
|
|
9
|
+
* They operate on plain JavaScript objects.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Flattens a hierarchical structure into a flat array of slides.
|
|
14
|
+
* @param {array} structure - The structure array (may contain sections with children)
|
|
15
|
+
* @returns {array} Flat array of slide objects
|
|
16
|
+
*/
|
|
17
|
+
export function flattenStructure(structure) {
|
|
18
|
+
const slides = [];
|
|
19
|
+
|
|
20
|
+
function traverse(items) {
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
if (item.children) {
|
|
23
|
+
traverse(item.children);
|
|
24
|
+
} else if (item.component) {
|
|
25
|
+
slides.push(item);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
traverse(structure);
|
|
31
|
+
return slides;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Registers an interaction ID and checks for duplicates.
|
|
36
|
+
* @param {string} id - The interaction ID to register
|
|
37
|
+
* @param {string} sourceName - The name of the source (e.g., slide ID)
|
|
38
|
+
* @param {string} sourceType - The type of interaction (e.g., 'DOM', 'Assessment')
|
|
39
|
+
* @param {Map} registry - The registry map
|
|
40
|
+
* @param {array} errors - The errors array to push to if duplicate found
|
|
41
|
+
*/
|
|
42
|
+
export function registerInteractionId(id, sourceName, sourceType, registry, errors) {
|
|
43
|
+
if (!id) return;
|
|
44
|
+
|
|
45
|
+
if (registry.has(id)) {
|
|
46
|
+
const existing = registry.get(id);
|
|
47
|
+
errors.push(`Duplicate ID "${id}": Found in ${sourceType} "${sourceName}" but already declared in ${existing.sourceType} "${existing.sourceName}". All interaction, assessment, and question IDs must be unique across the entire course.`);
|
|
48
|
+
} else {
|
|
49
|
+
registry.set(id, { sourceName, sourceType });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates global course configuration (objectives, orphans).
|
|
55
|
+
* @param {object} courseConfig - The full course configuration object
|
|
56
|
+
* @param {array} slides - Flattened array of slide objects
|
|
57
|
+
* @param {Set} slideFilesOnDisk - Set of slide file paths on disk (for orphan check)
|
|
58
|
+
* @returns {{ warnings: array, objectiveIds: Set }}
|
|
59
|
+
*/
|
|
60
|
+
export function validateGlobalConfig(courseConfig, slides, slideFilesOnDisk = new Set()) {
|
|
61
|
+
const warnings = [];
|
|
62
|
+
const slideComponentPaths = new Set(slides.map(s => s.component));
|
|
63
|
+
const allObjectiveIds = new Set();
|
|
64
|
+
const allSlideIds = new Set(slides.map(s => s.id));
|
|
65
|
+
|
|
66
|
+
// Check for orphaned slide files
|
|
67
|
+
for (const knownFile of slideFilesOnDisk) {
|
|
68
|
+
if (!slideComponentPaths.has(knownFile)) {
|
|
69
|
+
warnings.push(`Orphaned File: Slide module "${knownFile}" exists but is not used in the course structure.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate objectives
|
|
74
|
+
if (courseConfig.objectives && Array.isArray(courseConfig.objectives)) {
|
|
75
|
+
for (const objective of courseConfig.objectives) {
|
|
76
|
+
if (!objective.id) {
|
|
77
|
+
warnings.push('Objective missing required \'id\' property.');
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
allObjectiveIds.add(objective.id);
|
|
81
|
+
|
|
82
|
+
if (objective.criteria) {
|
|
83
|
+
const criteria = objective.criteria;
|
|
84
|
+
|
|
85
|
+
if (criteria.type === 'slideVisited' && criteria.slideId && !allSlideIds.has(criteria.slideId)) {
|
|
86
|
+
warnings.push(`Objective "${objective.id}" has 'slideVisited' criteria with an invalid slideId: "${criteria.slideId}".`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (criteria.type === 'allSlidesVisited' && Array.isArray(criteria.slideIds)) {
|
|
90
|
+
for (const slideId of criteria.slideIds) {
|
|
91
|
+
if (!allSlideIds.has(slideId)) {
|
|
92
|
+
warnings.push(`Objective "${objective.id}" has 'allSlidesVisited' criteria with an invalid slideId: "${slideId}".`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (criteria.type === 'timeOnSlide' && criteria.slideId && !allSlideIds.has(criteria.slideId)) {
|
|
98
|
+
warnings.push(`Objective "${objective.id}" has 'timeOnSlide' criteria with an invalid slideId: "${criteria.slideId}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { warnings, objectiveIds: allObjectiveIds };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validates assessment configuration.
|
|
109
|
+
* @param {object} assessmentConfig - The assessment configuration object
|
|
110
|
+
* @param {string} slideId - The slide identifier
|
|
111
|
+
* @param {Set} objectiveIds - Valid objective IDs
|
|
112
|
+
* @param {array} errors - Array to collect errors
|
|
113
|
+
* @param {array} warnings - Array to collect warnings
|
|
114
|
+
* @param {Map} interactionIdRegistry - Registry for checking duplicate IDs
|
|
115
|
+
*/
|
|
116
|
+
export function validateAssessmentConfig(assessmentConfig, slideId, objectiveIds, errors, warnings, interactionIdRegistry) {
|
|
117
|
+
// Basic structure validation
|
|
118
|
+
if (!assessmentConfig.id) {
|
|
119
|
+
errors.push(`[${slideId}] Assessment missing required 'id' property`);
|
|
120
|
+
} else {
|
|
121
|
+
registerInteractionId(assessmentConfig.id, slideId, 'Assessment', interactionIdRegistry, errors);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate assessmentObjective link
|
|
125
|
+
if (assessmentConfig.assessmentObjective) {
|
|
126
|
+
if (!objectiveIds.has(assessmentConfig.assessmentObjective)) {
|
|
127
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' has an invalid assessmentObjective: "${assessmentConfig.assessmentObjective}". This objective ID does not exist in the course configuration.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for runtime-defined questions (skip validation if so)
|
|
132
|
+
const hasRuntimeQuestions = assessmentConfig._hasRuntimeQuestions;
|
|
133
|
+
const hasRuntimeQuestionBanks = assessmentConfig._hasRuntimeQuestionBanks;
|
|
134
|
+
|
|
135
|
+
// Validate question source
|
|
136
|
+
const hasQuestions = Array.isArray(assessmentConfig.questions) && assessmentConfig.questions.length > 0;
|
|
137
|
+
const hasBanks = Array.isArray(assessmentConfig.questionBanks) && assessmentConfig.questionBanks.length > 0;
|
|
138
|
+
|
|
139
|
+
if (!hasRuntimeQuestions && !hasRuntimeQuestionBanks) {
|
|
140
|
+
if (!hasQuestions && !hasBanks) {
|
|
141
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' must have either 'questions' or 'questionBanks' array`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (hasQuestions && hasBanks) {
|
|
146
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' cannot have both 'questions' and 'questionBanks' - use one or the other`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate settings
|
|
151
|
+
const settings = assessmentConfig.settings || {};
|
|
152
|
+
|
|
153
|
+
if (settings.passingScore != null) {
|
|
154
|
+
if (typeof settings.passingScore !== 'number') {
|
|
155
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' passingScore must be a number, got ${typeof settings.passingScore}`);
|
|
156
|
+
} else if (settings.passingScore < 0 || settings.passingScore > 100) {
|
|
157
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' passingScore must be 0-100, got ${settings.passingScore}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (settings.randomizeQuestions !== undefined && typeof settings.randomizeQuestions !== 'boolean') {
|
|
162
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' randomizeQuestions must be boolean, got ${typeof settings.randomizeQuestions}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (settings.randomizeOnRetake !== undefined && typeof settings.randomizeOnRetake !== 'boolean') {
|
|
166
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' randomizeOnRetake must be boolean, got ${typeof settings.randomizeOnRetake}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate remedial/restart relationship
|
|
170
|
+
if (settings.attemptsBeforeRestart && settings.attemptsBeforeRemedial) {
|
|
171
|
+
if (settings.attemptsBeforeRestart <= settings.attemptsBeforeRemedial) {
|
|
172
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' attemptsBeforeRestart (${settings.attemptsBeforeRestart}) must be > attemptsBeforeRemedial (${settings.attemptsBeforeRemedial})`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (settings.attemptsBeforeRemedial && !settings.remedialSlideIds) {
|
|
177
|
+
errors.push(`[${slideId}] Assessment '${assessmentConfig.id}' has attemptsBeforeRemedial but no remedialSlideIds`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (settings.remedialSlideIds && settings.remedialSlideIds.length > 0 && !settings.attemptsBeforeRemedial) {
|
|
181
|
+
warnings.push(`[${slideId}] Assessment '${assessmentConfig.id}' has remedialSlideIds but no attemptsBeforeRemedial (slides won't be used)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Skip question validation if runtime-defined
|
|
185
|
+
if (hasRuntimeQuestions || hasRuntimeQuestionBanks) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate questions
|
|
190
|
+
if (hasQuestions) {
|
|
191
|
+
assessmentConfig.questions.forEach((q, idx) => {
|
|
192
|
+
validateQuestionConfig(q, `${slideId} Question ${idx + 1}`, errors, interactionIdRegistry);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate question banks
|
|
197
|
+
if (hasBanks) {
|
|
198
|
+
assessmentConfig.questionBanks.forEach((bank, bankIdx) => {
|
|
199
|
+
const bankRef = `${slideId} Bank ${bankIdx + 1}`;
|
|
200
|
+
|
|
201
|
+
if (!bank.id) {
|
|
202
|
+
errors.push(`${bankRef} missing required 'id' property`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(bank.questions) || bank.questions.length === 0) {
|
|
206
|
+
errors.push(`${bankRef} must have at least one question in 'questions' array`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (bank.selectCount == null) {
|
|
210
|
+
errors.push(`${bankRef} missing required 'selectCount' property`);
|
|
211
|
+
} else if (bank.selectCount !== 'all') {
|
|
212
|
+
if (typeof bank.selectCount !== 'number') {
|
|
213
|
+
errors.push(`${bankRef} selectCount must be number or 'all', got ${typeof bank.selectCount}`);
|
|
214
|
+
} else if (bank.selectCount <= 0) {
|
|
215
|
+
errors.push(`${bankRef} selectCount must be positive, got ${bank.selectCount}`);
|
|
216
|
+
} else if (bank.questions && bank.selectCount > bank.questions.length) {
|
|
217
|
+
errors.push(`${bankRef} selectCount (${bank.selectCount}) exceeds available questions (${bank.questions.length})`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (bank.questions) {
|
|
222
|
+
bank.questions.forEach((q, qIdx) => {
|
|
223
|
+
validateQuestionConfig(q, `${bankRef} Question ${qIdx + 1}`, errors, interactionIdRegistry);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validates a single question configuration.
|
|
232
|
+
* @param {object} question - The question configuration object
|
|
233
|
+
* @param {string} ref - Reference string for error messages
|
|
234
|
+
* @param {array} errors - Array to collect errors
|
|
235
|
+
* @param {Map} interactionIdRegistry - Registry for checking duplicate IDs
|
|
236
|
+
*/
|
|
237
|
+
export function validateQuestionConfig(question, ref, errors, interactionIdRegistry) {
|
|
238
|
+
if (!question.type) {
|
|
239
|
+
errors.push(`${ref} missing required 'type' property`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!question.prompt && !question.questionText) {
|
|
243
|
+
errors.push(`${ref} missing required 'prompt' property`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (question.weight == null) {
|
|
247
|
+
errors.push(`${ref} missing required 'weight' property`);
|
|
248
|
+
} else if (typeof question.weight === 'number' && question.weight <= 0) {
|
|
249
|
+
errors.push(`${ref} weight must be positive, got ${question.weight}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!question.id) {
|
|
253
|
+
errors.push(`${ref} missing required 'id' property`);
|
|
254
|
+
} else {
|
|
255
|
+
registerInteractionId(question.id, ref, 'Question', interactionIdRegistry, errors);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Type-specific validation
|
|
259
|
+
if (question.type === 'multiple-choice' || question.type === 'multiple-choice-single') {
|
|
260
|
+
if (!question.correctAnswer && !question.multiple) {
|
|
261
|
+
errors.push(`${ref} (${question.type}) missing required 'correctAnswer' property`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (question.type && question.type.startsWith('multiple-choice')) {
|
|
266
|
+
if (!Array.isArray(question.choices) || question.choices.length === 0) {
|
|
267
|
+
errors.push(`${ref} (${question.type}) must have at least one choice in 'choices' array`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (question.type === 'true-false') {
|
|
272
|
+
if (question.correctAnswer !== true && question.correctAnswer !== false) {
|
|
273
|
+
errors.push(`${ref} (true-false) correctAnswer must be boolean (true or false)`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (question.type === 'numeric') {
|
|
278
|
+
if (question.correctAnswer == null) {
|
|
279
|
+
errors.push(`${ref} (numeric) missing required 'correctAnswer' property`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates engagement configuration for a slide.
|
|
286
|
+
* @param {object} slide - The slide configuration
|
|
287
|
+
* @param {array} errors - Array to collect errors
|
|
288
|
+
* @param {array} warnings - Array to collect warnings
|
|
289
|
+
*/
|
|
290
|
+
export function validateEngagement(slide, errors, warnings) {
|
|
291
|
+
if (!slide.engagement) {
|
|
292
|
+
errors.push(`Slide "${slide.id}" (${slide.component}) is missing required 'engagement' configuration. Add "engagement: { required: false }" at minimum.`);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const engagement = slide.engagement;
|
|
297
|
+
|
|
298
|
+
if (engagement.required) {
|
|
299
|
+
if (!engagement.requirements || !Array.isArray(engagement.requirements)) {
|
|
300
|
+
errors.push(`Slide "${slide.id}" has engagement.required=true but no requirements array defined.`);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (engagement.requirements.length === 0) {
|
|
305
|
+
warnings.push(`Slide "${slide.id}" has engagement.required=true but empty requirements array. Set required=false if no tracking needed.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (engagement.mode && !['all', 'any'].includes(engagement.mode)) {
|
|
309
|
+
errors.push(`Slide "${slide.id}" has invalid engagement.mode "${engagement.mode}". Must be "all" or "any".`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Validates a requirement configuration (structure only, not content).
|
|
318
|
+
* Content validation (e.g., checking if tabs/accordion exist) is environment-specific.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} slideId - The slide identifier
|
|
321
|
+
* @param {object} requirement - The requirement configuration
|
|
322
|
+
* @param {array} errors - Array to collect errors
|
|
323
|
+
* @param {array} warnings - Array to collect warnings
|
|
324
|
+
* @param {object} engagementTrackingMap - Reverse map: engagementTracking value -> component type
|
|
325
|
+
*/
|
|
326
|
+
export function validateRequirementConfig(slideId, requirement, errors, warnings, engagementTrackingMap = {}) {
|
|
327
|
+
const type = requirement.type;
|
|
328
|
+
|
|
329
|
+
// Component-linked requirement types — auto-recognized from schemas.
|
|
330
|
+
// Content validation (does the component exist?) is environment-specific.
|
|
331
|
+
if (engagementTrackingMap[type]) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Config-only requirement types — validate required properties
|
|
336
|
+
switch (type) {
|
|
337
|
+
case 'interactionComplete':
|
|
338
|
+
if (!requirement.interactionId) {
|
|
339
|
+
errors.push(`Slide "${slideId}" has 'interactionComplete' requirement without interactionId.`);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'allInteractionsComplete':
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'scrollDepth': {
|
|
347
|
+
const percentage = requirement.percentage || requirement.minPercentage;
|
|
348
|
+
if (!percentage) {
|
|
349
|
+
errors.push(`Slide "${slideId}" has 'scrollDepth' requirement without percentage.`);
|
|
350
|
+
} else if (percentage < 0 || percentage > 100) {
|
|
351
|
+
errors.push(`Slide "${slideId}" scrollDepth percentage must be 0-100 (got ${percentage}).`);
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case 'timeOnSlide':
|
|
357
|
+
if (!requirement.seconds || requirement.seconds <= 0) {
|
|
358
|
+
errors.push(`Slide "${slideId}" has 'timeOnSlide' requirement without valid seconds value.`);
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'videoComplete':
|
|
363
|
+
if (!requirement.videoId) {
|
|
364
|
+
errors.push(`Slide "${slideId}" has 'videoComplete' requirement without videoId.`);
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case 'audioComplete':
|
|
369
|
+
if (!requirement.audioId) {
|
|
370
|
+
errors.push(`Slide "${slideId}" has 'audioComplete' requirement without audioId.`);
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'slideAudioComplete':
|
|
375
|
+
break;
|
|
376
|
+
|
|
377
|
+
case 'modalAudioComplete':
|
|
378
|
+
if (!requirement.modalId) {
|
|
379
|
+
errors.push(`Slide "${slideId}" has 'modalAudioComplete' requirement without modalId.`);
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case 'flag':
|
|
384
|
+
if (!requirement.key) {
|
|
385
|
+
errors.push(`Slide "${slideId}" has 'flag' requirement without key property.`);
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
case 'allFlags':
|
|
390
|
+
if (!requirement.flags || !Array.isArray(requirement.flags)) {
|
|
391
|
+
errors.push(`Slide "${slideId}" has 'allFlags' requirement without flags array.`);
|
|
392
|
+
} else if (requirement.flags.length === 0) {
|
|
393
|
+
errors.push(`Slide "${slideId}" has 'allFlags' requirement with empty flags array.`);
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
default:
|
|
398
|
+
warnings.push(`Slide "${slideId}" has unknown requirement type: "${type}".`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Valid gating condition types
|
|
404
|
+
*/
|
|
405
|
+
const VALID_GATING_CONDITION_TYPES = [
|
|
406
|
+
'objectiveStatus',
|
|
407
|
+
'assessmentStatus',
|
|
408
|
+
'assessmentAttempts',
|
|
409
|
+
'assessmentConfig',
|
|
410
|
+
'stateFlag',
|
|
411
|
+
'timeOnSlide',
|
|
412
|
+
'custom'
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Validates navigation gating conditions for a slide.
|
|
417
|
+
* @param {string} slideId - The slide identifier
|
|
418
|
+
* @param {object} gating - The gating configuration object
|
|
419
|
+
* @param {Set<string>} objectiveIds - Set of valid objective IDs
|
|
420
|
+
* @param {array} errors - Array to collect errors
|
|
421
|
+
*/
|
|
422
|
+
export function validateGatingConditions(slideId, gating, objectiveIds, errors) {
|
|
423
|
+
if (!gating.conditions || !Array.isArray(gating.conditions)) {
|
|
424
|
+
errors.push(`Slide "${slideId}" has navigation.gating but no conditions array.`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (gating.mode && !['all', 'any'].includes(gating.mode)) {
|
|
429
|
+
errors.push(`Slide "${slideId}" has invalid gating.mode "${gating.mode}". Must be "all" or "any".`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const condition of gating.conditions) {
|
|
433
|
+
if (!condition.type) {
|
|
434
|
+
errors.push(`Slide "${slideId}" has a gating condition without a type property.`);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!VALID_GATING_CONDITION_TYPES.includes(condition.type)) {
|
|
439
|
+
errors.push(
|
|
440
|
+
`Slide "${slideId}" has invalid gating condition type: "${condition.type}". ` +
|
|
441
|
+
`Valid types: ${VALID_GATING_CONDITION_TYPES.join(', ')}. ` +
|
|
442
|
+
'Note: \'slideVisited\' is only valid for objective criteria, not gating conditions.'
|
|
443
|
+
);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
switch (condition.type) {
|
|
448
|
+
case 'objectiveStatus':
|
|
449
|
+
if (!condition.objectiveId) {
|
|
450
|
+
errors.push(`Slide "${slideId}" has 'objectiveStatus' gating condition without objectiveId.`);
|
|
451
|
+
} else if (!objectiveIds.has(condition.objectiveId)) {
|
|
452
|
+
errors.push(`Slide "${slideId}" has 'objectiveStatus' gating condition with unknown objectiveId: "${condition.objectiveId}".`);
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case 'assessmentStatus':
|
|
457
|
+
case 'assessmentAttempts':
|
|
458
|
+
case 'assessmentConfig':
|
|
459
|
+
if (!condition.assessmentId) {
|
|
460
|
+
errors.push(`Slide "${slideId}" has '${condition.type}' gating condition without assessmentId.`);
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case 'stateFlag':
|
|
465
|
+
if (!condition.key) {
|
|
466
|
+
errors.push(`Slide "${slideId}" has 'stateFlag' gating condition without key property.`);
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'timeOnSlide':
|
|
471
|
+
if (!condition.minSeconds && condition.minSeconds !== 0) {
|
|
472
|
+
errors.push(`Slide "${slideId}" has 'timeOnSlide' gating condition without minSeconds property.`);
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case 'custom':
|
|
477
|
+
if (!condition.callback && typeof condition.evaluate !== 'function') {
|
|
478
|
+
errors.push(`Slide "${slideId}" has 'custom' gating condition without callback or evaluate function.`);
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Format lint results for display.
|
|
487
|
+
* @param {{ errors: string[], warnings: string[] }} results
|
|
488
|
+
* @returns {string} Formatted output
|
|
489
|
+
*/
|
|
490
|
+
export function formatLintResults({ errors, warnings }) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
|
|
493
|
+
if (warnings.length > 0) {
|
|
494
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
495
|
+
lines.push(' COURSE VALIDATION WARNINGS');
|
|
496
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
497
|
+
lines.push('');
|
|
498
|
+
warnings.forEach((w, i) => lines.push(`${i + 1}. ${w}`));
|
|
499
|
+
lines.push('');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (errors.length > 0) {
|
|
503
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
504
|
+
lines.push(' COURSE VALIDATION FAILED');
|
|
505
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
506
|
+
lines.push('');
|
|
507
|
+
errors.forEach((e, i) => lines.push(`${i + 1}. ${e}`));
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push('The course cannot be built until these errors are resolved.');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
513
|
+
lines.push('✅ Course validation passed with no errors or warnings.');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return lines.join('\n');
|
|
517
|
+
}
|