@lessonkit/core 1.5.0 → 1.7.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/dist/index.cjs CHANGED
@@ -21,18 +21,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ACCORDION_FORBIDDEN_CHILD_TYPES: () => ACCORDION_FORBIDDEN_CHILD_TYPES,
24
+ ASSESSMENT_17_CHILD_TYPES: () => ASSESSMENT_17_CHILD_TYPES,
24
25
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: () => ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
25
26
  BLOCKS_14_PAGE_SLIDE: () => BLOCKS_14_PAGE_SLIDE,
26
27
  BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES: () => BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
27
28
  BRANCH_NODE_ALLOWED_CHILD_TYPES: () => BRANCH_NODE_ALLOWED_CHILD_TYPES,
28
29
  COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
29
30
  COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
31
+ GAME_MAP_ALLOWED_CHILD_TYPES: () => GAME_MAP_ALLOWED_CHILD_TYPES,
30
32
  ID_MAX_LENGTH: () => ID_MAX_LENGTH,
31
33
  ID_PATTERN: () => ID_PATTERN,
32
34
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
33
35
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES: () => INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
36
+ MAP_STAGE_ALLOWED_CHILD_TYPES: () => MAP_STAGE_ALLOWED_CHILD_TYPES,
34
37
  PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
35
38
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
39
+ SINGLE_CHOICE_SET_ALLOWED_CHILD_TYPES: () => SINGLE_CHOICE_SET_ALLOWED_CHILD_TYPES,
36
40
  SLIDE_ALLOWED_CHILD_TYPES: () => SLIDE_ALLOWED_CHILD_TYPES,
37
41
  SLIDE_DECK_ALLOWED_CHILD_TYPES: () => SLIDE_DECK_ALLOWED_CHILD_TYPES,
38
42
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
@@ -77,6 +81,7 @@ __export(index_exports, {
77
81
  hasCourseStartedXapiSent: () => hasCourseStartedXapiSent,
78
82
  isChildTypeAllowed: () => isChildTypeAllowed,
79
83
  isLifecycleTelemetryEvent: () => isLifecycleTelemetryEvent,
84
+ isMultiSelectMcq: () => isMultiSelectMcq,
80
85
  loadCompoundState: () => loadCompoundState,
81
86
  markCourseStarted: () => markCourseStarted,
82
87
  markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
@@ -84,6 +89,7 @@ __export(index_exports, {
84
89
  markCourseStartedXapiSent: () => markCourseStartedXapiSent,
85
90
  migrateCourseStartedMark: () => migrateCourseStartedMark,
86
91
  nowIso: () => nowIso,
92
+ orderChoicesByIndices: () => orderChoicesByIndices,
87
93
  parseBlockId: () => parseBlockId,
88
94
  parseCheckId: () => parseCheckId,
89
95
  parseCompoundResumeState: () => parseCompoundResumeState,
@@ -92,8 +98,12 @@ __export(index_exports, {
92
98
  resetSharedVolatileSessionIdForTests: () => resetSharedVolatileSessionIdForTests,
93
99
  resetStoragePortForTests: () => resetStoragePortForTests,
94
100
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
101
+ resolveMcqCorrectAnswers: () => resolveMcqCorrectAnswers,
102
+ resolveMcqShuffleSeed: () => resolveMcqShuffleSeed,
95
103
  resolveSessionId: () => resolveSessionId,
96
104
  saveCompoundState: () => saveCompoundState,
105
+ scoreMcqSelection: () => scoreMcqSelection,
106
+ shuffleChoiceIndices: () => shuffleChoiceIndices,
97
107
  slugifyId: () => slugifyId,
98
108
  telemetryCatalogV2Version: () => telemetryCatalogV2Version,
99
109
  telemetryCatalogV3Version: () => telemetryCatalogV3Version,
@@ -240,7 +250,7 @@ function buildLessonkitUrn(parts) {
240
250
  urn += `:block:${blockId}`;
241
251
  }
242
252
  if (parts.nodeId !== void 0) {
243
- const nodeId = assertValidId(parts.nodeId, "blockId");
253
+ const nodeId = assertValidId(parts.nodeId, "nodeId");
244
254
  if (parts.blockId === void 0) {
245
255
  throw new Error("buildLessonkitUrn: nodeId requires blockId");
246
256
  }
@@ -249,6 +259,69 @@ function buildLessonkitUrn(parts) {
249
259
  return urn;
250
260
  }
251
261
 
262
+ // src/mcqAssessment.ts
263
+ function resolveMcqCorrectAnswers(props) {
264
+ if (props.answers && props.answers.length > 1) {
265
+ return new Set(props.answers.map((a) => a.trim()).filter(Boolean));
266
+ }
267
+ const single = props.answers?.[0]?.trim() ?? props.answer.trim();
268
+ return new Set(single ? [single] : []);
269
+ }
270
+ function isMultiSelectMcq(props) {
271
+ return (props.answers?.length ?? 0) > 1;
272
+ }
273
+ function scoreMcqSelection(selected, correct, multi, passingScore) {
274
+ const maxScore = multi ? Math.max(correct.size, 1) : 1;
275
+ if (!selected || Array.isArray(selected) && selected.length === 0) {
276
+ return {
277
+ score: 0,
278
+ maxScore,
279
+ exactMatch: false,
280
+ hasWrongSelection: false,
281
+ passedThreshold: false
282
+ };
283
+ }
284
+ const selectedSet = new Set(
285
+ (Array.isArray(selected) ? selected : [selected]).map((s) => s.trim()).filter(Boolean)
286
+ );
287
+ let score = 0;
288
+ for (const label of selectedSet) {
289
+ if (correct.has(label)) score += 1;
290
+ }
291
+ const hasWrongSelection = [...selectedSet].some((label) => !correct.has(label));
292
+ const exactMatch = !hasWrongSelection && selectedSet.size === correct.size && [...correct].every((label) => selectedSet.has(label));
293
+ const threshold = passingScore ?? maxScore;
294
+ const passedThreshold = score >= threshold && !hasWrongSelection;
295
+ return { score, maxScore, exactMatch, hasWrongSelection, passedThreshold };
296
+ }
297
+ function hashSeedToNumber(seed) {
298
+ if (typeof seed === "number" && Number.isFinite(seed)) return Math.abs(Math.trunc(seed)) || 1;
299
+ let hash = 2166136261;
300
+ const text = String(seed);
301
+ for (let i = 0; i < text.length; i += 1) {
302
+ hash ^= text.charCodeAt(i);
303
+ hash = Math.imul(hash, 16777619);
304
+ }
305
+ return hash >>> 0 || 1;
306
+ }
307
+ function shuffleChoiceIndices(length, seed) {
308
+ const indices = Array.from({ length }, (_, i) => i);
309
+ if (length <= 1) return indices;
310
+ let state = hashSeedToNumber(seed);
311
+ for (let i = length - 1; i > 0; i -= 1) {
312
+ state = Math.imul(state, 1664525) + 1013904223 >>> 0;
313
+ const j = state % (i + 1);
314
+ [indices[i], indices[j]] = [indices[j], indices[i]];
315
+ }
316
+ return indices;
317
+ }
318
+ function resolveMcqShuffleSeed(props) {
319
+ return props.shuffleSeed ?? props.checkId;
320
+ }
321
+ function orderChoicesByIndices(choices, orderIndices) {
322
+ return orderIndices.map((index) => choices[index] ?? "").filter((c) => c.length > 0);
323
+ }
324
+
252
325
  // src/compound.ts
253
326
  var COMPOUND_RESUME_SCHEMA_VERSION = 1;
254
327
  function createCompoundResumeState(input = {}) {
@@ -310,9 +383,11 @@ function parseCompoundResumeState(raw, opts) {
310
383
  opts?.onDroppedChildKeys?.(droppedChildKeys);
311
384
  }
312
385
  const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
386
+ const rawPageIndex = Math.max(0, Math.floor(obj.activePageIndex));
387
+ const activePageIndex = typeof opts?.pageCount === "number" && opts.pageCount > 0 ? clampCompoundPageIndex(rawPageIndex, opts.pageCount) : rawPageIndex;
313
388
  return {
314
389
  schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
315
- activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
390
+ activePageIndex,
316
391
  ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
317
392
  childStates
318
393
  };
@@ -362,6 +437,11 @@ function clearCompoundState(storage, courseId, compoundId) {
362
437
  }
363
438
 
364
439
  // src/compoundAllowlists.ts
440
+ var ASSESSMENT_17_CHILD_TYPES = [
441
+ "SortParagraphs",
442
+ "GuessTheAnswer",
443
+ "MultimediaChoice"
444
+ ];
365
445
  var PAGE_AND_SLIDE_14_BLOCKS = [
366
446
  "Video",
367
447
  "Summary",
@@ -406,7 +486,18 @@ var PAGE_ALLOWED_CHILD_TYPES = [
406
486
  "ImageSlider",
407
487
  "Embed",
408
488
  "Chart",
409
- "ProgressTracker"
489
+ "Table",
490
+ "ImageJuxtaposition",
491
+ "Timeline",
492
+ "ImageSequence",
493
+ "Collage",
494
+ "AudioRecorder",
495
+ "CombinationLock",
496
+ "QrContent",
497
+ "Crossword",
498
+ "AdventCalendar",
499
+ "ProgressTracker",
500
+ ...ASSESSMENT_17_CHILD_TYPES
410
501
  ];
411
502
  var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
412
503
  "Text",
@@ -440,9 +531,66 @@ var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
440
531
  "ImageSlider",
441
532
  "Embed",
442
533
  "Chart",
443
- "BranchChoice"
534
+ "Table",
535
+ "ImageJuxtaposition",
536
+ "Timeline",
537
+ "ImageSequence",
538
+ "Collage",
539
+ "AudioRecorder",
540
+ "CombinationLock",
541
+ "QrContent",
542
+ "Crossword",
543
+ "AdventCalendar",
544
+ "BranchChoice",
545
+ ...ASSESSMENT_17_CHILD_TYPES
444
546
  ];
445
547
  var BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES = ["BranchNode"];
548
+ var GAME_MAP_ALLOWED_CHILD_TYPES = ["MapStage"];
549
+ var MAP_STAGE_ALLOWED_CHILD_TYPES = [
550
+ "Text",
551
+ "Heading",
552
+ "Image",
553
+ "Video",
554
+ "Scenario",
555
+ "Reflection",
556
+ "Quiz",
557
+ "KnowledgeCheck",
558
+ "TrueFalse",
559
+ "FillInTheBlanks",
560
+ "DragAndDrop",
561
+ "DragTheWords",
562
+ "MarkTheWords",
563
+ "Summary",
564
+ "ImagePairing",
565
+ "ImageSequencing",
566
+ "MemoryGame",
567
+ "InformationWall",
568
+ "ParallaxSlideshow",
569
+ "Questionnaire",
570
+ "Essay",
571
+ "ArithmeticQuiz",
572
+ "Accordion",
573
+ "DialogCards",
574
+ "Flashcards",
575
+ "ImageHotspots",
576
+ "FindHotspot",
577
+ "FindMultipleHotspots",
578
+ "ImageSlider",
579
+ "Embed",
580
+ "Chart",
581
+ "Table",
582
+ "ImageJuxtaposition",
583
+ "Timeline",
584
+ "ImageSequence",
585
+ "Collage",
586
+ "AudioRecorder",
587
+ "CombinationLock",
588
+ "QrContent",
589
+ "Crossword",
590
+ "AdventCalendar",
591
+ "MapExit",
592
+ ...ASSESSMENT_17_CHILD_TYPES
593
+ ];
446
594
  var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
447
595
  var SLIDE_ALLOWED_CHILD_TYPES = [
448
596
  "Text",
@@ -475,7 +623,18 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
475
623
  "FindMultipleHotspots",
476
624
  "ImageSlider",
477
625
  "Embed",
478
- "Chart"
626
+ "Chart",
627
+ "Table",
628
+ "ImageJuxtaposition",
629
+ "Timeline",
630
+ "ImageSequence",
631
+ "Collage",
632
+ "AudioRecorder",
633
+ "CombinationLock",
634
+ "QrContent",
635
+ "Crossword",
636
+ "AdventCalendar",
637
+ ...ASSESSMENT_17_CHILD_TYPES
479
638
  ];
480
639
  var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
481
640
  var TIMED_CUE_ALLOWED_CHILD_TYPES = [
@@ -491,7 +650,9 @@ var TIMED_CUE_ALLOWED_CHILD_TYPES = [
491
650
  "MemoryGame",
492
651
  "Questionnaire",
493
652
  "Essay",
494
- "ArithmeticQuiz"
653
+ "ArithmeticQuiz",
654
+ "MultimediaChoice",
655
+ "GuessTheAnswer"
495
656
  ];
496
657
  var INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES = ["TimedCue"];
497
658
  var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
@@ -508,8 +669,10 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
508
669
  "ImagePairing",
509
670
  "ImageSequencing",
510
671
  "ArithmeticQuiz",
511
- "Essay"
672
+ "Essay",
673
+ ...ASSESSMENT_17_CHILD_TYPES
512
674
  ];
675
+ var SINGLE_CHOICE_SET_ALLOWED_CHILD_TYPES = ["Quiz", "KnowledgeCheck"];
513
676
  var ALLOWLISTS = {
514
677
  Page: PAGE_ALLOWED_CHILD_TYPES,
515
678
  InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
@@ -519,7 +682,10 @@ var ALLOWLISTS = {
519
682
  InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
520
683
  AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
521
684
  BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
522
- BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES
685
+ BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES,
686
+ GameMap: GAME_MAP_ALLOWED_CHILD_TYPES,
687
+ MapStage: MAP_STAGE_ALLOWED_CHILD_TYPES,
688
+ SingleChoiceSet: SINGLE_CHOICE_SET_ALLOWED_CHILD_TYPES
523
689
  };
524
690
  var COMPOUND_MAX_NESTING_DEPTH = {
525
691
  Page: 1,
@@ -530,7 +696,10 @@ var COMPOUND_MAX_NESTING_DEPTH = {
530
696
  InteractiveVideo: 2,
531
697
  AssessmentSequence: 1,
532
698
  BranchingScenario: 2,
533
- BranchNode: 1
699
+ BranchNode: 1,
700
+ GameMap: 2,
701
+ MapStage: 1,
702
+ SingleChoiceSet: 1
534
703
  };
535
704
  function getAllowedChildTypes(parent) {
536
705
  return ALLOWLISTS[parent];
@@ -835,6 +1004,78 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
835
1004
  dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
836
1005
  xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
837
1006
  urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{toNodeId}"
1007
+ },
1008
+ {
1009
+ name: "image_juxtaposition_changed",
1010
+ description: "Learner adjusted the before/after divider",
1011
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1012
+ dataFields: ["blockId", "position"],
1013
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
1014
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1015
+ },
1016
+ {
1017
+ name: "timeline_event_viewed",
1018
+ description: "Learner focused a timeline event",
1019
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1020
+ dataFields: ["blockId", "eventId"],
1021
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
1022
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1023
+ },
1024
+ {
1025
+ name: "image_sequence_changed",
1026
+ description: "Learner changed the image sequence frame",
1027
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1028
+ dataFields: ["blockId", "frameIndex"],
1029
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
1030
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1031
+ },
1032
+ {
1033
+ name: "audio_recording_started",
1034
+ description: "Learner started an audio recording",
1035
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1036
+ dataFields: ["blockId"],
1037
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
1038
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1039
+ },
1040
+ {
1041
+ name: "audio_recording_completed",
1042
+ description: "Learner completed an audio recording",
1043
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1044
+ dataFields: ["blockId"],
1045
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
1046
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1047
+ },
1048
+ {
1049
+ name: "qr_content_revealed",
1050
+ description: "Learner revealed QR hidden content",
1051
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1052
+ dataFields: ["blockId"],
1053
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
1054
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1055
+ },
1056
+ {
1057
+ name: "advent_door_opened",
1058
+ description: "Learner opened an advent calendar door",
1059
+ requiredFields: ["courseId", "sessionId", "timestamp"],
1060
+ dataFields: ["blockId", "doorId", "day"],
1061
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
1062
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
1063
+ },
1064
+ {
1065
+ name: "map_stage_viewed",
1066
+ description: "Learner viewed a stage in a GameMap",
1067
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
1068
+ dataFields: ["blockId", "stageId", "stageIndex", "stageLabel"],
1069
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
1070
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{stageId}"
1071
+ },
1072
+ {
1073
+ name: "map_exit_selected",
1074
+ description: "Learner selected a map exit in a GameMap",
1075
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
1076
+ dataFields: ["blockId", "fromStageId", "toStageId", "label", "scoreWeight"],
1077
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
1078
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{toStageId}"
838
1079
  }
839
1080
  ];
840
1081
  function buildTelemetryCatalogV3() {
@@ -931,6 +1172,10 @@ function createTrackingClient(opts) {
931
1172
  const runFlush = () => {
932
1173
  if (!buffer.length) return Promise.resolve(true);
933
1174
  const events = buffer.splice(0, buffer.length);
1175
+ for (const event of events) {
1176
+ const key = eventDedupKey(event);
1177
+ if (key) pendingDeliverIds.add(key);
1178
+ }
934
1179
  let succeeded = false;
935
1180
  return Promise.resolve().then(async () => {
936
1181
  if (batchSink) {
@@ -940,7 +1185,7 @@ function createTrackingClient(opts) {
940
1185
  try {
941
1186
  await sink?.(events[i]);
942
1187
  } catch {
943
- buffer.unshift(...events.slice(i));
1188
+ buffer.unshift(...events);
944
1189
  return;
945
1190
  }
946
1191
  }
@@ -1064,7 +1309,8 @@ function randomSessionIdFallback() {
1064
1309
  if (g.crypto?.getRandomValues) {
1065
1310
  const bytes = new Uint8Array(16);
1066
1311
  g.crypto.getRandomValues(bytes);
1067
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1312
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1313
+ return `s-${hex}`;
1068
1314
  }
1069
1315
  throw new Error(
1070
1316
  "[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
@@ -1072,7 +1318,9 @@ function randomSessionIdFallback() {
1072
1318
  }
1073
1319
  function createSessionId() {
1074
1320
  const g = globalThis;
1075
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
1321
+ if (g.crypto?.randomUUID) {
1322
+ return `s-${g.crypto.randomUUID().replace(/-/g, "")}`;
1323
+ }
1076
1324
  return randomSessionIdFallback();
1077
1325
  }
1078
1326
 
@@ -1380,6 +1628,90 @@ var TELEMETRY_EVENT_REGISTRY = {
1380
1628
  data: opts.data
1381
1629
  };
1382
1630
  }
1631
+ },
1632
+ image_juxtaposition_changed: {
1633
+ build: (opts, base) => ({
1634
+ name: "image_juxtaposition_changed",
1635
+ ...base,
1636
+ lessonId: opts.lessonId,
1637
+ data: opts.data
1638
+ })
1639
+ },
1640
+ timeline_event_viewed: {
1641
+ build: (opts, base) => ({
1642
+ name: "timeline_event_viewed",
1643
+ ...base,
1644
+ lessonId: opts.lessonId,
1645
+ data: opts.data
1646
+ })
1647
+ },
1648
+ image_sequence_changed: {
1649
+ build: (opts, base) => ({
1650
+ name: "image_sequence_changed",
1651
+ ...base,
1652
+ lessonId: opts.lessonId,
1653
+ data: opts.data
1654
+ })
1655
+ },
1656
+ audio_recording_started: {
1657
+ build: (opts, base) => ({
1658
+ name: "audio_recording_started",
1659
+ ...base,
1660
+ lessonId: opts.lessonId,
1661
+ data: opts.data
1662
+ })
1663
+ },
1664
+ audio_recording_completed: {
1665
+ build: (opts, base) => ({
1666
+ name: "audio_recording_completed",
1667
+ ...base,
1668
+ lessonId: opts.lessonId,
1669
+ data: opts.data
1670
+ })
1671
+ },
1672
+ qr_content_revealed: {
1673
+ build: (opts, base) => ({
1674
+ name: "qr_content_revealed",
1675
+ ...base,
1676
+ lessonId: opts.lessonId,
1677
+ data: opts.data
1678
+ })
1679
+ },
1680
+ advent_door_opened: {
1681
+ build: (opts, base) => ({
1682
+ name: "advent_door_opened",
1683
+ ...base,
1684
+ lessonId: opts.lessonId,
1685
+ data: opts.data
1686
+ })
1687
+ },
1688
+ map_stage_viewed: {
1689
+ requiresLessonId: true,
1690
+ build: (opts, base) => {
1691
+ if (opts.name !== "map_stage_viewed") throw new Error("unexpected event");
1692
+ const lessonId = opts.lessonId;
1693
+ if (!lessonId) throw new Error("map_stage_viewed requires active lessonId");
1694
+ return {
1695
+ name: "map_stage_viewed",
1696
+ ...base,
1697
+ lessonId,
1698
+ data: opts.data
1699
+ };
1700
+ }
1701
+ },
1702
+ map_exit_selected: {
1703
+ requiresLessonId: true,
1704
+ build: (opts, base) => {
1705
+ if (opts.name !== "map_exit_selected") throw new Error("unexpected event");
1706
+ const lessonId = opts.lessonId;
1707
+ if (!lessonId) throw new Error("map_exit_selected requires active lessonId");
1708
+ return {
1709
+ name: "map_exit_selected",
1710
+ ...base,
1711
+ lessonId,
1712
+ data: opts.data
1713
+ };
1714
+ }
1383
1715
  }
1384
1716
  };
1385
1717
  function buildTelemetryEventFromRegistry(opts) {
@@ -1540,26 +1872,35 @@ function createMemoryBackedSessionStorage(session) {
1540
1872
  );
1541
1873
  }
1542
1874
  };
1875
+ const bypassCacheForKey = (key) => key === "lessonkit:sessionId" || key.startsWith("lessonkit:course_started");
1543
1876
  return {
1544
1877
  getItem: (key) => {
1545
1878
  if (tombstones.has(key)) return null;
1546
- if (memory.has(key)) return memory.get(key);
1879
+ if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
1547
1880
  try {
1548
1881
  const value = session.getItem(key);
1549
- if (value !== null) memory.set(key, value);
1550
- return value;
1882
+ if (value !== null) {
1883
+ memory.set(key, value);
1884
+ return value;
1885
+ }
1886
+ if (bypassCacheForKey(key) && memory.has(key)) {
1887
+ return memory.get(key);
1888
+ }
1889
+ if (bypassCacheForKey(key)) memory.delete(key);
1890
+ return null;
1551
1891
  } catch {
1552
1892
  return memory.get(key) ?? null;
1553
1893
  }
1554
1894
  },
1555
1895
  setItem: (key, value) => {
1556
1896
  tombstones.delete(key);
1557
- memory.set(key, value);
1558
1897
  try {
1559
1898
  session.setItem(key, value);
1899
+ memory.set(key, value);
1560
1900
  return true;
1561
1901
  } catch {
1562
1902
  warnPersistFailure();
1903
+ memory.set(key, value);
1563
1904
  return false;
1564
1905
  }
1565
1906
  },
@@ -1672,6 +2013,29 @@ function createProgressController() {
1672
2013
  // src/session.ts
1673
2014
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
1674
2015
  var volatileSessionIds = /* @__PURE__ */ new WeakMap();
2016
+ var volatileStorageMarks = /* @__PURE__ */ new WeakMap();
2017
+ function rememberVolatileMark(storage, key) {
2018
+ let keys = volatileStorageMarks.get(storage);
2019
+ if (!keys) {
2020
+ keys = /* @__PURE__ */ new Set();
2021
+ volatileStorageMarks.set(storage, keys);
2022
+ }
2023
+ keys.add(key);
2024
+ }
2025
+ function hasVolatileMark(storage, key) {
2026
+ return volatileStorageMarks.get(storage)?.has(key) ?? false;
2027
+ }
2028
+ function clearVolatileMark(storage, key) {
2029
+ volatileStorageMarks.get(storage)?.delete(key);
2030
+ }
2031
+ function storageHasMark(storage, key) {
2032
+ return storage.getItem(key) === "1" || hasVolatileMark(storage, key);
2033
+ }
2034
+ function storageSetMark(storage, key) {
2035
+ const persisted = storage.setItem(key, "1");
2036
+ if (!persisted) rememberVolatileMark(storage, key);
2037
+ return persisted;
2038
+ }
1675
2039
  function isDevEnvironment2() {
1676
2040
  const g = globalThis;
1677
2041
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
@@ -1687,21 +2051,7 @@ function sessionKeySegment(sessionId) {
1687
2051
  const validated = validateId(sessionId);
1688
2052
  return validated.ok ? validated.id : encodeURIComponent(sessionId);
1689
2053
  }
1690
- function resolveSessionId(storage, provided) {
1691
- if (provided !== void 0) {
1692
- const trimmed = provided.trim();
1693
- if (trimmed.length > 0) {
1694
- const validated = validateId(trimmed);
1695
- if (validated.ok) return validated.id;
1696
- if (isDevEnvironment2()) {
1697
- console.warn(
1698
- `[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
1699
- );
1700
- }
1701
- }
1702
- }
1703
- const existing = storage.getItem(SESSION_STORAGE_KEY);
1704
- if (existing) return existing;
2054
+ function resolveGeneratedSessionId(storage) {
1705
2055
  const volatile = volatileSessionIds.get(storage);
1706
2056
  if (volatile) return volatile;
1707
2057
  const id = createSessionId();
@@ -1717,6 +2067,50 @@ function resolveSessionId(storage, provided) {
1717
2067
  }
1718
2068
  return id;
1719
2069
  }
2070
+ function resolveFallbackSessionId(storage, options) {
2071
+ const existing = storage.getItem(SESSION_STORAGE_KEY);
2072
+ if (existing) {
2073
+ const trimmedExisting = existing.trim();
2074
+ const validatedExisting = validateId(trimmedExisting);
2075
+ if (validatedExisting.ok) return validatedExisting.id;
2076
+ storage.removeItem?.(SESSION_STORAGE_KEY);
2077
+ if (isDevEnvironment2()) {
2078
+ console.warn(
2079
+ `[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
2080
+ );
2081
+ }
2082
+ const fallback = resolveGeneratedSessionId(storage);
2083
+ options?.onInvalidSessionId?.({
2084
+ invalidId: existing,
2085
+ fallbackId: fallback,
2086
+ source: "stored"
2087
+ });
2088
+ return fallback;
2089
+ }
2090
+ return resolveGeneratedSessionId(storage);
2091
+ }
2092
+ function resolveSessionId(storage, provided, options) {
2093
+ if (provided !== void 0) {
2094
+ const trimmed = provided.trim();
2095
+ if (trimmed.length > 0) {
2096
+ const validated = validateId(trimmed);
2097
+ if (validated.ok) return validated.id;
2098
+ if (isDevEnvironment2()) {
2099
+ console.warn(
2100
+ `[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
2101
+ );
2102
+ }
2103
+ const fallback = resolveFallbackSessionId(storage, options);
2104
+ options?.onInvalidSessionId?.({
2105
+ invalidId: trimmed,
2106
+ fallbackId: fallback,
2107
+ source: "provided"
2108
+ });
2109
+ return fallback;
2110
+ }
2111
+ }
2112
+ return resolveFallbackSessionId(storage, options);
2113
+ }
1720
2114
  function courseStartedStorageKey(sessionId, courseId) {
1721
2115
  return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
1722
2116
  }
@@ -1731,35 +2125,35 @@ function courseStartedXapiStorageKey(sessionId, courseId) {
1731
2125
  }
1732
2126
  function hasCourseStarted(storage, sessionId, courseId) {
1733
2127
  if (!courseId) return false;
1734
- return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
2128
+ return storageHasMark(storage, courseStartedStorageKey(sessionId, courseId));
1735
2129
  }
1736
2130
  function markCourseStarted(storage, sessionId, courseId) {
1737
2131
  if (!courseId) return false;
1738
- return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
2132
+ return storageSetMark(storage, courseStartedStorageKey(sessionId, courseId));
1739
2133
  }
1740
2134
  function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1741
2135
  if (!courseId) return false;
1742
- return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
2136
+ return storageHasMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
1743
2137
  }
1744
2138
  function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1745
2139
  if (!courseId) return false;
1746
- return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
2140
+ return storageSetMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
1747
2141
  }
1748
2142
  function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1749
2143
  if (!courseId) return false;
1750
- return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
2144
+ return storageHasMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
1751
2145
  }
1752
2146
  function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1753
2147
  if (!courseId) return false;
1754
- return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
2148
+ return storageSetMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
1755
2149
  }
1756
2150
  function hasCourseStartedXapiSent(storage, sessionId, courseId) {
1757
2151
  if (!courseId) return false;
1758
- return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
2152
+ return storageHasMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
1759
2153
  }
1760
2154
  function markCourseStartedXapiSent(storage, sessionId, courseId) {
1761
2155
  if (!courseId) return false;
1762
- return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
2156
+ return storageSetMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
1763
2157
  }
1764
2158
  function resetSharedVolatileSessionIdForTests() {
1765
2159
  }
@@ -1767,6 +2161,7 @@ function migrateStorageMark(storage, fromKey, toKey, hasMark) {
1767
2161
  if (!hasMark) return;
1768
2162
  if (storage.setItem(toKey, "1")) {
1769
2163
  storage.removeItem?.(fromKey);
2164
+ clearVolatileMark(storage, fromKey);
1770
2165
  }
1771
2166
  }
1772
2167
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
@@ -1799,11 +2194,28 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
1799
2194
 
1800
2195
  // src/runtime/courseLifecycle.ts
1801
2196
  var courseStartedEmitFlights = /* @__PURE__ */ new Map();
2197
+ var courseStartedEmittedInTab = /* @__PURE__ */ new Set();
1802
2198
  function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1803
2199
  const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
1804
2200
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
2201
+ if (courseStartedEmittedInTab.has(flightKey)) {
2202
+ return Promise.resolve({
2203
+ emitted: true,
2204
+ marked: marked || markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
2205
+ });
2206
+ }
1805
2207
  if (alreadyEmittedToSink) {
1806
- return Promise.resolve({ emitted: true, marked });
2208
+ const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
2209
+ return Promise.resolve({
2210
+ emitted: true,
2211
+ marked: markPersisted
2212
+ });
2213
+ }
2214
+ if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
2215
+ return Promise.resolve({
2216
+ emitted: true,
2217
+ marked: true
2218
+ });
1807
2219
  }
1808
2220
  const existing = courseStartedEmitFlights.get(flightKey);
1809
2221
  if (existing) {
@@ -1812,15 +2224,19 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1812
2224
  const flight = Promise.resolve().then(() => {
1813
2225
  try {
1814
2226
  const emitted = deps.emitCourseStartedEvent(ctx);
1815
- if (emitted && !marked) {
1816
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
2227
+ if (emitted) {
2228
+ courseStartedEmittedInTab.add(flightKey);
1817
2229
  }
2230
+ const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
1818
2231
  return {
1819
2232
  emitted,
1820
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
2233
+ marked: markPersisted
1821
2234
  };
1822
2235
  } catch {
1823
- return { emitted: false, marked };
2236
+ return {
2237
+ emitted: false,
2238
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
2239
+ };
1824
2240
  } finally {
1825
2241
  if (courseStartedEmitFlights.get(flightKey) === flight) {
1826
2242
  courseStartedEmitFlights.delete(flightKey);
@@ -1856,7 +2272,16 @@ function completeCourseWithTelemetry(opts) {
1856
2272
  });
1857
2273
  }
1858
2274
  const result = opts.progress.completeCourse();
1859
- if (!result.didComplete) return false;
2275
+ if (!result.didComplete) {
2276
+ const after = opts.progress.getState();
2277
+ if (after.activeLessonId) {
2278
+ const lessonResult = opts.progress.completeLesson(after.activeLessonId, opts.nowMs);
2279
+ if (lessonResult.didComplete) {
2280
+ opts.emitLessonCompleted(after.activeLessonId, lessonResult.durationMs);
2281
+ }
2282
+ }
2283
+ return false;
2284
+ }
1860
2285
  opts.emitCourseCompleted();
1861
2286
  return true;
1862
2287
  }
@@ -1979,6 +2404,10 @@ function resolvePluginHost(plugins) {
1979
2404
  if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
1980
2405
  return null;
1981
2406
  }
2407
+ function pluginListFingerprint(plugins) {
2408
+ if (!plugins || !Array.isArray(plugins)) return null;
2409
+ return plugins.map((p) => `${p.id}\0${p.version ?? ""}`).join("\n");
2410
+ }
1982
2411
  function warnRuntimeV1Deprecated() {
1983
2412
  const g = globalThis;
1984
2413
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
@@ -1991,12 +2420,19 @@ function createLessonkitRuntime(config, ports = {}) {
1991
2420
  const storage = ports.storage ?? createSessionStoragePort();
1992
2421
  const clock = ports.clock ?? createDefaultClock();
1993
2422
  const configSnapshot = { ...config };
2423
+ const hasExplicitSessionId = Boolean(configSnapshot.session?.sessionId?.trim());
2424
+ let autoSessionId;
1994
2425
  let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
2426
+ if (!hasExplicitSessionId) {
2427
+ autoSessionId = sessionId;
2428
+ }
1995
2429
  let attemptId = configSnapshot.session?.attemptId;
1996
2430
  let user = configSnapshot.session?.user;
1997
2431
  let courseId = configSnapshot.courseId;
2432
+ let configuredSessionId = configSnapshot.session?.sessionId;
1998
2433
  let progress = createProgressController();
1999
2434
  let pluginHost = resolvePluginHost(configSnapshot.plugins);
2435
+ let pluginFingerprint = pluginListFingerprint(configSnapshot.plugins);
2000
2436
  let disposed = false;
2001
2437
  const getPluginCtx = () => buildPluginContext({
2002
2438
  courseId,
@@ -2008,12 +2444,6 @@ function createLessonkitRuntime(config, ports = {}) {
2008
2444
  pluginHost?.setupAll(getPluginCtx());
2009
2445
  }
2010
2446
  const getSession = () => ({ sessionId, attemptId, user });
2011
- const syncSessionFromConfig = (next) => {
2012
- sessionId = resolveSessionId(storage, next.session?.sessionId);
2013
- attemptId = next.session?.attemptId;
2014
- user = next.session?.user;
2015
- courseId = next.courseId;
2016
- };
2017
2447
  const applyPluginsToEvent = (event) => {
2018
2448
  if (!pluginHost) return event;
2019
2449
  return pluginHost.runTelemetry(event, getPluginCtx());
@@ -2035,7 +2465,6 @@ function createLessonkitRuntime(config, ports = {}) {
2035
2465
  const event = buildAndApply(name, data, lessonId);
2036
2466
  if (event) emitFn(event);
2037
2467
  };
2038
- syncSessionFromConfig(configSnapshot);
2039
2468
  const track = (name, data, emit, lessonId) => {
2040
2469
  if (disposed) return;
2041
2470
  const event = buildAndApply(name, data, lessonId);
@@ -2077,21 +2506,52 @@ function createLessonkitRuntime(config, ports = {}) {
2077
2506
  if (next.autoCompleteOnLessonSwitch !== void 0) {
2078
2507
  configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
2079
2508
  }
2509
+ if (next.courseId !== void 0) {
2510
+ courseId = next.courseId;
2511
+ }
2080
2512
  if (next.session !== void 0) {
2513
+ const previousSessionId = sessionId;
2081
2514
  configSnapshot.session = { ...configSnapshot.session, ...next.session };
2515
+ const explicitSessionId = configSnapshot.session?.sessionId?.trim();
2516
+ if (explicitSessionId) {
2517
+ sessionId = resolveSessionId(storage, explicitSessionId);
2518
+ autoSessionId = void 0;
2519
+ } else {
2520
+ sessionId = autoSessionId ?? resolveSessionId(storage, void 0);
2521
+ if (!autoSessionId) autoSessionId = sessionId;
2522
+ }
2523
+ attemptId = configSnapshot.session?.attemptId;
2524
+ user = configSnapshot.session?.user;
2525
+ if (previousSessionId !== sessionId) {
2526
+ const prevExplicit = configuredSessionId?.trim();
2527
+ const nextExplicit = configSnapshot.session?.sessionId?.trim();
2528
+ const isExplicitLearnerSwap = Boolean(prevExplicit) && Boolean(nextExplicit) && prevExplicit !== nextExplicit;
2529
+ if (!isExplicitLearnerSwap) {
2530
+ migrateCourseStartedMark(storage, previousSessionId, sessionId, courseId);
2531
+ }
2532
+ }
2533
+ configuredSessionId = configSnapshot.session?.sessionId;
2082
2534
  }
2083
- syncSessionFromConfig(configSnapshot);
2084
2535
  const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
2085
2536
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
2086
2537
  progress = createProgressController();
2087
- }
2088
- if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
2089
- pluginHost?.disposeAll();
2090
- configSnapshot.plugins = next.plugins;
2091
- pluginHost = resolvePluginHost(configSnapshot.plugins);
2092
2538
  if (!configSnapshot.deferPluginSetup) {
2539
+ pluginHost?.disposeAll();
2093
2540
  pluginHost?.setupAll(getPluginCtx());
2094
2541
  }
2542
+ }
2543
+ if (next.plugins !== void 0) {
2544
+ const nextFingerprint = pluginListFingerprint(next.plugins);
2545
+ const pluginsChanged = next.plugins !== configSnapshot.plugins || nextFingerprint !== null && nextFingerprint !== pluginFingerprint;
2546
+ if (pluginsChanged) {
2547
+ pluginHost?.disposeAll();
2548
+ configSnapshot.plugins = next.plugins;
2549
+ pluginFingerprint = nextFingerprint;
2550
+ pluginHost = resolvePluginHost(configSnapshot.plugins);
2551
+ if (!configSnapshot.deferPluginSetup) {
2552
+ pluginHost?.setupAll(getPluginCtx());
2553
+ }
2554
+ }
2095
2555
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
2096
2556
  pluginHost.disposeAll();
2097
2557
  pluginHost.setupAll(getPluginCtx());
@@ -2146,15 +2606,17 @@ function createLessonkitRuntime(config, ports = {}) {
2146
2606
  configSnapshot.courseId = nextCourseId;
2147
2607
  courseId = nextCourseId;
2148
2608
  progress = createProgressController();
2149
- pluginHost?.disposeAll();
2150
2609
  if (!configSnapshot.deferPluginSetup) {
2610
+ pluginHost?.disposeAll();
2151
2611
  pluginHost?.setupAll(getPluginCtx());
2152
2612
  }
2153
2613
  },
2154
2614
  dispose() {
2155
2615
  if (disposed) return;
2156
2616
  disposed = true;
2157
- pluginHost?.disposeAll();
2617
+ if (!configSnapshot.deferPluginSetup) {
2618
+ pluginHost?.disposeAll();
2619
+ }
2158
2620
  }
2159
2621
  };
2160
2622
  }
@@ -2172,18 +2634,22 @@ function defineLifecyclePlugin(plugin) {
2172
2634
  // Annotate the CommonJS export names for ESM import in node:
2173
2635
  0 && (module.exports = {
2174
2636
  ACCORDION_FORBIDDEN_CHILD_TYPES,
2637
+ ASSESSMENT_17_CHILD_TYPES,
2175
2638
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
2176
2639
  BLOCKS_14_PAGE_SLIDE,
2177
2640
  BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
2178
2641
  BRANCH_NODE_ALLOWED_CHILD_TYPES,
2179
2642
  COMPOUND_MAX_NESTING_DEPTH,
2180
2643
  COMPOUND_RESUME_SCHEMA_VERSION,
2644
+ GAME_MAP_ALLOWED_CHILD_TYPES,
2181
2645
  ID_MAX_LENGTH,
2182
2646
  ID_PATTERN,
2183
2647
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
2184
2648
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
2649
+ MAP_STAGE_ALLOWED_CHILD_TYPES,
2185
2650
  PAGE_ALLOWED_CHILD_TYPES,
2186
2651
  SESSION_STORAGE_KEY,
2652
+ SINGLE_CHOICE_SET_ALLOWED_CHILD_TYPES,
2187
2653
  SLIDE_ALLOWED_CHILD_TYPES,
2188
2654
  SLIDE_DECK_ALLOWED_CHILD_TYPES,
2189
2655
  TELEMETRY_EVENT_CATALOG,
@@ -2228,6 +2694,7 @@ function defineLifecyclePlugin(plugin) {
2228
2694
  hasCourseStartedXapiSent,
2229
2695
  isChildTypeAllowed,
2230
2696
  isLifecycleTelemetryEvent,
2697
+ isMultiSelectMcq,
2231
2698
  loadCompoundState,
2232
2699
  markCourseStarted,
2233
2700
  markCourseStartedEmittedToTracking,
@@ -2235,6 +2702,7 @@ function defineLifecyclePlugin(plugin) {
2235
2702
  markCourseStartedXapiSent,
2236
2703
  migrateCourseStartedMark,
2237
2704
  nowIso,
2705
+ orderChoicesByIndices,
2238
2706
  parseBlockId,
2239
2707
  parseCheckId,
2240
2708
  parseCompoundResumeState,
@@ -2243,8 +2711,12 @@ function defineLifecyclePlugin(plugin) {
2243
2711
  resetSharedVolatileSessionIdForTests,
2244
2712
  resetStoragePortForTests,
2245
2713
  resetTelemetryBuilderWarningsForTests,
2714
+ resolveMcqCorrectAnswers,
2715
+ resolveMcqShuffleSeed,
2246
2716
  resolveSessionId,
2247
2717
  saveCompoundState,
2718
+ scoreMcqSelection,
2719
+ shuffleChoiceIndices,
2248
2720
  slugifyId,
2249
2721
  telemetryCatalogV2Version,
2250
2722
  telemetryCatalogV3Version,