@lessonkit/core 1.5.0 → 1.6.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.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  tryEmitCourseStarted,
37
37
  validateId,
38
38
  warnDev
39
- } from "./chunk-KFXFQ6B2.js";
39
+ } from "./chunk-NGCHHJSM.js";
40
40
 
41
41
  // src/assertNever.ts
42
42
  function assertNever(value, message = "Unexpected value") {
@@ -117,7 +117,7 @@ function buildLessonkitUrn(parts) {
117
117
  urn += `:block:${blockId}`;
118
118
  }
119
119
  if (parts.nodeId !== void 0) {
120
- const nodeId = assertValidId(parts.nodeId, "blockId");
120
+ const nodeId = assertValidId(parts.nodeId, "nodeId");
121
121
  if (parts.blockId === void 0) {
122
122
  throw new Error("buildLessonkitUrn: nodeId requires blockId");
123
123
  }
@@ -187,9 +187,11 @@ function parseCompoundResumeState(raw, opts) {
187
187
  opts?.onDroppedChildKeys?.(droppedChildKeys);
188
188
  }
189
189
  const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
190
+ const rawPageIndex = Math.max(0, Math.floor(obj.activePageIndex));
191
+ const activePageIndex = typeof opts?.pageCount === "number" && opts.pageCount > 0 ? clampCompoundPageIndex(rawPageIndex, opts.pageCount) : rawPageIndex;
190
192
  return {
191
193
  schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
192
- activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
194
+ activePageIndex,
193
195
  ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
194
196
  childStates
195
197
  };
@@ -273,6 +275,16 @@ var PAGE_ALLOWED_CHILD_TYPES = [
273
275
  "ImageSlider",
274
276
  "Embed",
275
277
  "Chart",
278
+ "Table",
279
+ "ImageJuxtaposition",
280
+ "Timeline",
281
+ "ImageSequence",
282
+ "Collage",
283
+ "AudioRecorder",
284
+ "CombinationLock",
285
+ "QrContent",
286
+ "Crossword",
287
+ "AdventCalendar",
276
288
  "ProgressTracker"
277
289
  ];
278
290
  var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
@@ -307,9 +319,64 @@ var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
307
319
  "ImageSlider",
308
320
  "Embed",
309
321
  "Chart",
322
+ "Table",
323
+ "ImageJuxtaposition",
324
+ "Timeline",
325
+ "ImageSequence",
326
+ "Collage",
327
+ "AudioRecorder",
328
+ "CombinationLock",
329
+ "QrContent",
330
+ "Crossword",
331
+ "AdventCalendar",
310
332
  "BranchChoice"
311
333
  ];
312
334
  var BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES = ["BranchNode"];
335
+ var GAME_MAP_ALLOWED_CHILD_TYPES = ["MapStage"];
336
+ var MAP_STAGE_ALLOWED_CHILD_TYPES = [
337
+ "Text",
338
+ "Heading",
339
+ "Image",
340
+ "Video",
341
+ "Scenario",
342
+ "Reflection",
343
+ "Quiz",
344
+ "KnowledgeCheck",
345
+ "TrueFalse",
346
+ "FillInTheBlanks",
347
+ "DragAndDrop",
348
+ "DragTheWords",
349
+ "MarkTheWords",
350
+ "Summary",
351
+ "ImagePairing",
352
+ "ImageSequencing",
353
+ "MemoryGame",
354
+ "InformationWall",
355
+ "ParallaxSlideshow",
356
+ "Questionnaire",
357
+ "Essay",
358
+ "ArithmeticQuiz",
359
+ "Accordion",
360
+ "DialogCards",
361
+ "Flashcards",
362
+ "ImageHotspots",
363
+ "FindHotspot",
364
+ "FindMultipleHotspots",
365
+ "ImageSlider",
366
+ "Embed",
367
+ "Chart",
368
+ "Table",
369
+ "ImageJuxtaposition",
370
+ "Timeline",
371
+ "ImageSequence",
372
+ "Collage",
373
+ "AudioRecorder",
374
+ "CombinationLock",
375
+ "QrContent",
376
+ "Crossword",
377
+ "AdventCalendar",
378
+ "MapExit"
379
+ ];
313
380
  var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
314
381
  var SLIDE_ALLOWED_CHILD_TYPES = [
315
382
  "Text",
@@ -342,7 +409,17 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
342
409
  "FindMultipleHotspots",
343
410
  "ImageSlider",
344
411
  "Embed",
345
- "Chart"
412
+ "Chart",
413
+ "Table",
414
+ "ImageJuxtaposition",
415
+ "Timeline",
416
+ "ImageSequence",
417
+ "Collage",
418
+ "AudioRecorder",
419
+ "CombinationLock",
420
+ "QrContent",
421
+ "Crossword",
422
+ "AdventCalendar"
346
423
  ];
347
424
  var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
348
425
  var TIMED_CUE_ALLOWED_CHILD_TYPES = [
@@ -386,7 +463,9 @@ var ALLOWLISTS = {
386
463
  InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
387
464
  AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
388
465
  BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
389
- BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES
466
+ BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES,
467
+ GameMap: GAME_MAP_ALLOWED_CHILD_TYPES,
468
+ MapStage: MAP_STAGE_ALLOWED_CHILD_TYPES
390
469
  };
391
470
  var COMPOUND_MAX_NESTING_DEPTH = {
392
471
  Page: 1,
@@ -397,7 +476,9 @@ var COMPOUND_MAX_NESTING_DEPTH = {
397
476
  InteractiveVideo: 2,
398
477
  AssessmentSequence: 1,
399
478
  BranchingScenario: 2,
400
- BranchNode: 1
479
+ BranchNode: 1,
480
+ GameMap: 2,
481
+ MapStage: 1
401
482
  };
402
483
  function getAllowedChildTypes(parent) {
403
484
  return ALLOWLISTS[parent];
@@ -702,6 +783,78 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
702
783
  dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
703
784
  xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
704
785
  urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{toNodeId}"
786
+ },
787
+ {
788
+ name: "image_juxtaposition_changed",
789
+ description: "Learner adjusted the before/after divider",
790
+ requiredFields: ["courseId", "sessionId", "timestamp"],
791
+ dataFields: ["blockId", "position"],
792
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
793
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
794
+ },
795
+ {
796
+ name: "timeline_event_viewed",
797
+ description: "Learner focused a timeline event",
798
+ requiredFields: ["courseId", "sessionId", "timestamp"],
799
+ dataFields: ["blockId", "eventId"],
800
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
801
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
802
+ },
803
+ {
804
+ name: "image_sequence_changed",
805
+ description: "Learner changed the image sequence frame",
806
+ requiredFields: ["courseId", "sessionId", "timestamp"],
807
+ dataFields: ["blockId", "frameIndex"],
808
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
809
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
810
+ },
811
+ {
812
+ name: "audio_recording_started",
813
+ description: "Learner started an audio recording",
814
+ requiredFields: ["courseId", "sessionId", "timestamp"],
815
+ dataFields: ["blockId"],
816
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
817
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
818
+ },
819
+ {
820
+ name: "audio_recording_completed",
821
+ description: "Learner completed an audio recording",
822
+ requiredFields: ["courseId", "sessionId", "timestamp"],
823
+ dataFields: ["blockId"],
824
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
825
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
826
+ },
827
+ {
828
+ name: "qr_content_revealed",
829
+ description: "Learner revealed QR hidden content",
830
+ requiredFields: ["courseId", "sessionId", "timestamp"],
831
+ dataFields: ["blockId"],
832
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
833
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
834
+ },
835
+ {
836
+ name: "advent_door_opened",
837
+ description: "Learner opened an advent calendar door",
838
+ requiredFields: ["courseId", "sessionId", "timestamp"],
839
+ dataFields: ["blockId", "doorId", "day"],
840
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
841
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
842
+ },
843
+ {
844
+ name: "map_stage_viewed",
845
+ description: "Learner viewed a stage in a GameMap",
846
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
847
+ dataFields: ["blockId", "stageId", "stageIndex", "stageLabel"],
848
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
849
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{stageId}"
850
+ },
851
+ {
852
+ name: "map_exit_selected",
853
+ description: "Learner selected a map exit in a GameMap",
854
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
855
+ dataFields: ["blockId", "fromStageId", "toStageId", "label", "scoreWeight"],
856
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
857
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{toStageId}"
705
858
  }
706
859
  ];
707
860
  function buildTelemetryCatalogV3() {
@@ -798,6 +951,10 @@ function createTrackingClient(opts) {
798
951
  const runFlush = () => {
799
952
  if (!buffer.length) return Promise.resolve(true);
800
953
  const events = buffer.splice(0, buffer.length);
954
+ for (const event of events) {
955
+ const key = eventDedupKey(event);
956
+ if (key) pendingDeliverIds.add(key);
957
+ }
801
958
  let succeeded = false;
802
959
  return Promise.resolve().then(async () => {
803
960
  if (batchSink) {
@@ -807,7 +964,7 @@ function createTrackingClient(opts) {
807
964
  try {
808
965
  await sink?.(events[i]);
809
966
  } catch {
810
- buffer.unshift(...events.slice(i));
967
+ buffer.unshift(...events);
811
968
  return;
812
969
  }
813
970
  }
@@ -1142,6 +1299,10 @@ function resolvePluginHost(plugins) {
1142
1299
  if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
1143
1300
  return null;
1144
1301
  }
1302
+ function pluginListFingerprint(plugins) {
1303
+ if (!plugins || !Array.isArray(plugins)) return null;
1304
+ return plugins.map((p) => `${p.id}\0${p.version ?? ""}`).join("\n");
1305
+ }
1145
1306
  function warnRuntimeV1Deprecated() {
1146
1307
  const g = globalThis;
1147
1308
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
@@ -1154,12 +1315,19 @@ function createLessonkitRuntime(config, ports = {}) {
1154
1315
  const storage = ports.storage ?? createSessionStoragePort();
1155
1316
  const clock = ports.clock ?? createDefaultClock();
1156
1317
  const configSnapshot = { ...config };
1318
+ const hasExplicitSessionId = Boolean(configSnapshot.session?.sessionId?.trim());
1319
+ let autoSessionId;
1157
1320
  let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
1321
+ if (!hasExplicitSessionId) {
1322
+ autoSessionId = sessionId;
1323
+ }
1158
1324
  let attemptId = configSnapshot.session?.attemptId;
1159
1325
  let user = configSnapshot.session?.user;
1160
1326
  let courseId = configSnapshot.courseId;
1327
+ let configuredSessionId = configSnapshot.session?.sessionId;
1161
1328
  let progress = createProgressController();
1162
1329
  let pluginHost = resolvePluginHost(configSnapshot.plugins);
1330
+ let pluginFingerprint = pluginListFingerprint(configSnapshot.plugins);
1163
1331
  let disposed = false;
1164
1332
  const getPluginCtx = () => buildPluginContext({
1165
1333
  courseId,
@@ -1171,12 +1339,6 @@ function createLessonkitRuntime(config, ports = {}) {
1171
1339
  pluginHost?.setupAll(getPluginCtx());
1172
1340
  }
1173
1341
  const getSession = () => ({ sessionId, attemptId, user });
1174
- const syncSessionFromConfig = (next) => {
1175
- sessionId = resolveSessionId(storage, next.session?.sessionId);
1176
- attemptId = next.session?.attemptId;
1177
- user = next.session?.user;
1178
- courseId = next.courseId;
1179
- };
1180
1342
  const applyPluginsToEvent = (event) => {
1181
1343
  if (!pluginHost) return event;
1182
1344
  return pluginHost.runTelemetry(event, getPluginCtx());
@@ -1198,7 +1360,6 @@ function createLessonkitRuntime(config, ports = {}) {
1198
1360
  const event = buildAndApply(name, data, lessonId);
1199
1361
  if (event) emitFn(event);
1200
1362
  };
1201
- syncSessionFromConfig(configSnapshot);
1202
1363
  const track = (name, data, emit, lessonId) => {
1203
1364
  if (disposed) return;
1204
1365
  const event = buildAndApply(name, data, lessonId);
@@ -1240,21 +1401,52 @@ function createLessonkitRuntime(config, ports = {}) {
1240
1401
  if (next.autoCompleteOnLessonSwitch !== void 0) {
1241
1402
  configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
1242
1403
  }
1404
+ if (next.courseId !== void 0) {
1405
+ courseId = next.courseId;
1406
+ }
1243
1407
  if (next.session !== void 0) {
1408
+ const previousSessionId = sessionId;
1244
1409
  configSnapshot.session = { ...configSnapshot.session, ...next.session };
1410
+ const explicitSessionId = configSnapshot.session?.sessionId?.trim();
1411
+ if (explicitSessionId) {
1412
+ sessionId = resolveSessionId(storage, explicitSessionId);
1413
+ autoSessionId = void 0;
1414
+ } else {
1415
+ sessionId = autoSessionId ?? resolveSessionId(storage, void 0);
1416
+ if (!autoSessionId) autoSessionId = sessionId;
1417
+ }
1418
+ attemptId = configSnapshot.session?.attemptId;
1419
+ user = configSnapshot.session?.user;
1420
+ if (previousSessionId !== sessionId) {
1421
+ const prevExplicit = configuredSessionId?.trim();
1422
+ const nextExplicit = configSnapshot.session?.sessionId?.trim();
1423
+ const isExplicitLearnerSwap = Boolean(prevExplicit) && Boolean(nextExplicit) && prevExplicit !== nextExplicit;
1424
+ if (!isExplicitLearnerSwap) {
1425
+ migrateCourseStartedMark(storage, previousSessionId, sessionId, courseId);
1426
+ }
1427
+ }
1428
+ configuredSessionId = configSnapshot.session?.sessionId;
1245
1429
  }
1246
- syncSessionFromConfig(configSnapshot);
1247
1430
  const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
1248
1431
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
1249
1432
  progress = createProgressController();
1250
- }
1251
- if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
1252
- pluginHost?.disposeAll();
1253
- configSnapshot.plugins = next.plugins;
1254
- pluginHost = resolvePluginHost(configSnapshot.plugins);
1255
1433
  if (!configSnapshot.deferPluginSetup) {
1434
+ pluginHost?.disposeAll();
1256
1435
  pluginHost?.setupAll(getPluginCtx());
1257
1436
  }
1437
+ }
1438
+ if (next.plugins !== void 0) {
1439
+ const nextFingerprint = pluginListFingerprint(next.plugins);
1440
+ const pluginsChanged = next.plugins !== configSnapshot.plugins || nextFingerprint !== null && nextFingerprint !== pluginFingerprint;
1441
+ if (pluginsChanged) {
1442
+ pluginHost?.disposeAll();
1443
+ configSnapshot.plugins = next.plugins;
1444
+ pluginFingerprint = nextFingerprint;
1445
+ pluginHost = resolvePluginHost(configSnapshot.plugins);
1446
+ if (!configSnapshot.deferPluginSetup) {
1447
+ pluginHost?.setupAll(getPluginCtx());
1448
+ }
1449
+ }
1258
1450
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
1259
1451
  pluginHost.disposeAll();
1260
1452
  pluginHost.setupAll(getPluginCtx());
@@ -1309,15 +1501,17 @@ function createLessonkitRuntime(config, ports = {}) {
1309
1501
  configSnapshot.courseId = nextCourseId;
1310
1502
  courseId = nextCourseId;
1311
1503
  progress = createProgressController();
1312
- pluginHost?.disposeAll();
1313
1504
  if (!configSnapshot.deferPluginSetup) {
1505
+ pluginHost?.disposeAll();
1314
1506
  pluginHost?.setupAll(getPluginCtx());
1315
1507
  }
1316
1508
  },
1317
1509
  dispose() {
1318
1510
  if (disposed) return;
1319
1511
  disposed = true;
1320
- pluginHost?.disposeAll();
1512
+ if (!configSnapshot.deferPluginSetup) {
1513
+ pluginHost?.disposeAll();
1514
+ }
1321
1515
  }
1322
1516
  };
1323
1517
  }
@@ -1340,10 +1534,12 @@ export {
1340
1534
  BRANCH_NODE_ALLOWED_CHILD_TYPES,
1341
1535
  COMPOUND_MAX_NESTING_DEPTH,
1342
1536
  COMPOUND_RESUME_SCHEMA_VERSION,
1537
+ GAME_MAP_ALLOWED_CHILD_TYPES,
1343
1538
  ID_MAX_LENGTH,
1344
1539
  ID_PATTERN,
1345
1540
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1346
1541
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
1542
+ MAP_STAGE_ALLOWED_CHILD_TYPES,
1347
1543
  PAGE_ALLOWED_CHILD_TYPES,
1348
1544
  SESSION_STORAGE_KEY,
1349
1545
  SLIDE_ALLOWED_CHILD_TYPES,
@@ -21,7 +21,7 @@ declare const ID_PATTERN: RegExp;
21
21
  declare const ID_MAX_LENGTH = 64;
22
22
 
23
23
  /** H5P-aligned interaction kinds for assessment telemetry and xAPI. */
24
- type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame";
24
+ type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame" | "combinationLock" | "crossword" | "wordSearch";
25
25
  /** Serializable resume blob for a single assessment block. */
26
26
  type AssessmentResumeState = Record<string, unknown>;
27
27
  /** Behaviour flags aligned with H5P question types. */
@@ -65,7 +65,7 @@ type McqAssessmentProps = AssessmentBaseProps & {
65
65
  answer: string;
66
66
  };
67
67
 
68
- type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted" | "branch_node_viewed" | "branch_selected";
68
+ type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted" | "branch_node_viewed" | "branch_selected" | "image_juxtaposition_changed" | "timeline_event_viewed" | "image_sequence_changed" | "audio_recording_started" | "audio_recording_completed" | "qr_content_revealed" | "advent_door_opened" | "map_stage_viewed" | "map_exit_selected";
69
69
  type TelemetryUser = {
70
70
  id?: string;
71
71
  email?: string;
@@ -196,6 +196,42 @@ type BranchSelectedData = {
196
196
  label: string;
197
197
  scoreWeight?: number;
198
198
  };
199
+ type ImageJuxtapositionChangedData = {
200
+ blockId: BlockId;
201
+ position: number;
202
+ };
203
+ type TimelineEventViewedData = {
204
+ blockId: BlockId;
205
+ eventId: string;
206
+ };
207
+ type ImageSequenceChangedData = {
208
+ blockId: BlockId;
209
+ frameIndex: number;
210
+ };
211
+ type AudioRecordingData = {
212
+ blockId: BlockId;
213
+ };
214
+ type QrContentRevealedData = {
215
+ blockId: BlockId;
216
+ };
217
+ type AdventDoorOpenedData = {
218
+ blockId: BlockId;
219
+ doorId: string;
220
+ day: number;
221
+ };
222
+ type MapStageViewedData = {
223
+ blockId: BlockId;
224
+ stageId: string;
225
+ stageIndex: number;
226
+ stageLabel?: string;
227
+ };
228
+ type MapExitSelectedData = {
229
+ blockId: BlockId;
230
+ fromStageId: string;
231
+ toStageId: string;
232
+ label: string;
233
+ scoreWeight?: number;
234
+ };
199
235
  type TelemetryEvent = (TelemetryEventBase & {
200
236
  name: "course_started";
201
237
  lessonId?: LessonId;
@@ -296,6 +332,42 @@ type TelemetryEvent = (TelemetryEventBase & {
296
332
  name: "branch_selected";
297
333
  lessonId: LessonId;
298
334
  data: BranchSelectedData;
335
+ }) | (TelemetryEventBase & {
336
+ name: "image_juxtaposition_changed";
337
+ lessonId?: LessonId;
338
+ data: ImageJuxtapositionChangedData;
339
+ }) | (TelemetryEventBase & {
340
+ name: "timeline_event_viewed";
341
+ lessonId?: LessonId;
342
+ data: TimelineEventViewedData;
343
+ }) | (TelemetryEventBase & {
344
+ name: "image_sequence_changed";
345
+ lessonId?: LessonId;
346
+ data: ImageSequenceChangedData;
347
+ }) | (TelemetryEventBase & {
348
+ name: "audio_recording_started";
349
+ lessonId?: LessonId;
350
+ data: AudioRecordingData;
351
+ }) | (TelemetryEventBase & {
352
+ name: "audio_recording_completed";
353
+ lessonId?: LessonId;
354
+ data: AudioRecordingData;
355
+ }) | (TelemetryEventBase & {
356
+ name: "qr_content_revealed";
357
+ lessonId?: LessonId;
358
+ data: QrContentRevealedData;
359
+ }) | (TelemetryEventBase & {
360
+ name: "advent_door_opened";
361
+ lessonId?: LessonId;
362
+ data: AdventDoorOpenedData;
363
+ }) | (TelemetryEventBase & {
364
+ name: "map_stage_viewed";
365
+ lessonId: LessonId;
366
+ data: MapStageViewedData;
367
+ }) | (TelemetryEventBase & {
368
+ name: "map_exit_selected";
369
+ lessonId: LessonId;
370
+ data: MapExitSelectedData;
299
371
  });
300
372
  /** Payload shape for a telemetry event name. */
301
373
  type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
@@ -446,6 +518,42 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
446
518
  name: "branch_selected";
447
519
  lessonId?: LessonId;
448
520
  data: BranchSelectedData;
521
+ }) | (BuildTelemetryEventContext & {
522
+ name: "image_juxtaposition_changed";
523
+ lessonId?: LessonId;
524
+ data: ImageJuxtapositionChangedData;
525
+ }) | (BuildTelemetryEventContext & {
526
+ name: "timeline_event_viewed";
527
+ lessonId?: LessonId;
528
+ data: TimelineEventViewedData;
529
+ }) | (BuildTelemetryEventContext & {
530
+ name: "image_sequence_changed";
531
+ lessonId?: LessonId;
532
+ data: ImageSequenceChangedData;
533
+ }) | (BuildTelemetryEventContext & {
534
+ name: "audio_recording_started";
535
+ lessonId?: LessonId;
536
+ data: AudioRecordingData;
537
+ }) | (BuildTelemetryEventContext & {
538
+ name: "audio_recording_completed";
539
+ lessonId?: LessonId;
540
+ data: AudioRecordingData;
541
+ }) | (BuildTelemetryEventContext & {
542
+ name: "qr_content_revealed";
543
+ lessonId?: LessonId;
544
+ data: QrContentRevealedData;
545
+ }) | (BuildTelemetryEventContext & {
546
+ name: "advent_door_opened";
547
+ lessonId?: LessonId;
548
+ data: AdventDoorOpenedData;
549
+ }) | (BuildTelemetryEventContext & {
550
+ name: "map_stage_viewed";
551
+ lessonId?: LessonId;
552
+ data: MapStageViewedData;
553
+ }) | (BuildTelemetryEventContext & {
554
+ name: "map_exit_selected";
555
+ lessonId?: LessonId;
556
+ data: MapExitSelectedData;
449
557
  });
450
558
 
451
559
  /** Reset dev-warning state (tests only). */
@@ -453,6 +561,18 @@ declare function resetTelemetryBuilderWarningsForTests(): void;
453
561
  /**
454
562
  * Build a typed telemetry event from a catalog event name and context.
455
563
  * Validates lesson-scoped events require `lessonId`.
564
+ *
565
+ * @example
566
+ * ```ts
567
+ * import { buildTelemetryEvent } from "@lessonkit/core";
568
+ *
569
+ * const event = buildTelemetryEvent({
570
+ * name: "lesson_completed",
571
+ * courseId: "sec-101",
572
+ * lessonId: "phishing-101",
573
+ * sessionId: "tab-abc",
574
+ * });
575
+ * ```
456
576
  */
457
577
  declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent;
458
578
  /**
@@ -592,12 +712,38 @@ declare function tryEmitCourseStarted(ctx: CourseLifecycleContext, deps: CourseL
592
712
  }>;
593
713
  declare function buildCourseStartedTelemetryEvent(ctx: CourseLifecycleContext): TelemetryEvent;
594
714
  type LessonCompletionEmitter = (lessonId: LessonId, durationMs?: number) => void;
715
+ /**
716
+ * Mark a lesson complete in progress state and emit `lesson_completed` when newly completed.
717
+ *
718
+ * @example
719
+ * ```ts
720
+ * completeLessonWithTelemetry({
721
+ * progress,
722
+ * lessonId: "lesson-1",
723
+ * nowMs: Date.now(),
724
+ * emitLessonCompleted: (id, durationMs) => track("lesson_completed", { lessonId: id, durationMs }),
725
+ * });
726
+ * ```
727
+ */
595
728
  declare function completeLessonWithTelemetry(opts: {
596
729
  progress: ProgressController;
597
730
  lessonId: LessonId;
598
731
  nowMs: number;
599
732
  emitLessonCompleted: LessonCompletionEmitter;
600
733
  }): boolean;
734
+ /**
735
+ * Complete the active lesson (if any), then mark the course complete and emit `course_completed`.
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * completeCourseWithTelemetry({
740
+ * progress,
741
+ * nowMs: Date.now(),
742
+ * emitLessonCompleted: (id) => track("lesson_completed", { lessonId: id }),
743
+ * emitCourseCompleted: () => track("course_completed", {}),
744
+ * });
745
+ * ```
746
+ */
601
747
  declare function completeCourseWithTelemetry(opts: {
602
748
  progress: ProgressController;
603
749
  nowMs: number;