@lessonkit/core 1.0.2 → 1.2.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
@@ -20,18 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACCORDION_FORBIDDEN_CHILD_TYPES: () => ACCORDION_FORBIDDEN_CHILD_TYPES,
24
+ ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: () => ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
25
+ COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
26
+ COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
23
27
  ID_MAX_LENGTH: () => ID_MAX_LENGTH,
24
28
  ID_PATTERN: () => ID_PATTERN,
29
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
30
+ PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
25
31
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
26
32
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
33
+ TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
34
+ TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
27
35
  assertNever: () => assertNever,
28
36
  assertValidId: () => assertValidId,
29
37
  buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
30
38
  buildLessonkitUrn: () => buildLessonkitUrn,
39
+ buildPluginContext: () => buildPluginContext,
31
40
  buildTelemetryCatalog: () => buildTelemetryCatalog,
41
+ buildTelemetryCatalogV2: () => buildTelemetryCatalogV2,
42
+ buildTelemetryCatalogV3: () => buildTelemetryCatalogV3,
32
43
  buildTelemetryEvent: () => buildTelemetryEvent,
44
+ clampCompoundPageIndex: () => clampCompoundPageIndex,
45
+ clearCompoundState: () => clearCompoundState,
33
46
  completeCourseWithTelemetry: () => completeCourseWithTelemetry,
34
47
  completeLessonWithTelemetry: () => completeLessonWithTelemetry,
48
+ compoundStateStorageKey: () => compoundStateStorageKey,
49
+ createCompoundResumeState: () => createCompoundResumeState,
35
50
  createDefaultClock: () => createDefaultClock,
36
51
  createGlobalTimer: () => createGlobalTimer,
37
52
  createLessonkitRuntime: () => createLessonkitRuntime,
@@ -47,10 +62,13 @@ __export(index_exports, {
47
62
  defineLifecyclePlugin: () => defineLifecyclePlugin,
48
63
  defineTelemetryPlugin: () => defineTelemetryPlugin,
49
64
  deriveId: () => deriveId,
65
+ getAllowedChildTypes: () => getAllowedChildTypes,
50
66
  getTabSessionId: () => getTabSessionId,
51
67
  hasCourseStarted: () => hasCourseStarted,
52
68
  hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
53
69
  hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
70
+ isChildTypeAllowed: () => isChildTypeAllowed,
71
+ loadCompoundState: () => loadCompoundState,
54
72
  markCourseStarted: () => markCourseStarted,
55
73
  markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
56
74
  markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
@@ -58,12 +76,16 @@ __export(index_exports, {
58
76
  nowIso: () => nowIso,
59
77
  parseBlockId: () => parseBlockId,
60
78
  parseCheckId: () => parseCheckId,
79
+ parseCompoundResumeState: () => parseCompoundResumeState,
61
80
  parseCourseId: () => parseCourseId,
62
81
  parseLessonId: () => parseLessonId,
63
82
  resetStoragePortForTests: () => resetStoragePortForTests,
64
83
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
65
84
  resolveSessionId: () => resolveSessionId,
85
+ saveCompoundState: () => saveCompoundState,
66
86
  slugifyId: () => slugifyId,
87
+ telemetryCatalogV2Version: () => telemetryCatalogV2Version,
88
+ telemetryCatalogV3Version: () => telemetryCatalogV3Version,
67
89
  telemetryCatalogVersion: () => telemetryCatalogVersion,
68
90
  tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
69
91
  tryEmitCourseStarted: () => tryEmitCourseStarted,
@@ -175,6 +197,122 @@ function buildLessonkitUrn(parts) {
175
197
  return urn;
176
198
  }
177
199
 
200
+ // src/compound.ts
201
+ var COMPOUND_RESUME_SCHEMA_VERSION = 1;
202
+ function createCompoundResumeState(input = {}) {
203
+ const childStates = {};
204
+ if (input.childStates) {
205
+ for (const [key, value] of Object.entries(input.childStates)) {
206
+ childStates[key] = value;
207
+ }
208
+ }
209
+ return {
210
+ schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
211
+ activePageIndex: input.activePageIndex ?? 0,
212
+ ...input.activeChapterIndex !== void 0 ? { activeChapterIndex: input.activeChapterIndex } : {},
213
+ childStates
214
+ };
215
+ }
216
+ function clampCompoundPageIndex(index, pageCount) {
217
+ if (pageCount < 1) return 0;
218
+ return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
219
+ }
220
+ function parseCompoundResumeState(raw) {
221
+ if (!raw || typeof raw !== "object") return null;
222
+ const obj = raw;
223
+ if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
224
+ if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
225
+ const childStates = {};
226
+ if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
227
+ for (const [key, value] of Object.entries(obj.childStates)) {
228
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
229
+ childStates[key] = value;
230
+ }
231
+ }
232
+ }
233
+ const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
234
+ return {
235
+ schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
236
+ activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
237
+ ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
238
+ childStates
239
+ };
240
+ }
241
+
242
+ // src/compoundState.ts
243
+ var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
244
+ function compoundStateStorageKey(courseId, compoundId) {
245
+ return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
246
+ }
247
+ function loadCompoundState(storage, courseId, compoundId) {
248
+ const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
249
+ if (!raw) return null;
250
+ try {
251
+ return parseCompoundResumeState(JSON.parse(raw));
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function saveCompoundState(storage, courseId, compoundId, state) {
257
+ storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
258
+ }
259
+ function clearCompoundState(storage, courseId, compoundId) {
260
+ storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
261
+ }
262
+
263
+ // src/compoundAllowlists.ts
264
+ var PAGE_ALLOWED_CHILD_TYPES = [
265
+ "Text",
266
+ "Heading",
267
+ "Image",
268
+ "Scenario",
269
+ "Reflection",
270
+ "Quiz",
271
+ "KnowledgeCheck",
272
+ "TrueFalse",
273
+ "FillInTheBlanks",
274
+ "DragAndDrop",
275
+ "DragTheWords",
276
+ "MarkTheWords",
277
+ "Accordion",
278
+ "DialogCards",
279
+ "Flashcards",
280
+ "ImageHotspots",
281
+ "FindHotspot",
282
+ "FindMultipleHotspots",
283
+ "ImageSlider",
284
+ "ProgressTracker"
285
+ ];
286
+ var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
287
+ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
288
+ "TrueFalse",
289
+ "FillInTheBlanks",
290
+ "DragAndDrop",
291
+ "DragTheWords",
292
+ "MarkTheWords",
293
+ "Quiz",
294
+ "KnowledgeCheck",
295
+ "FindHotspot",
296
+ "FindMultipleHotspots"
297
+ ];
298
+ var ALLOWLISTS = {
299
+ Page: PAGE_ALLOWED_CHILD_TYPES,
300
+ InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
301
+ AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
302
+ };
303
+ var COMPOUND_MAX_NESTING_DEPTH = {
304
+ Page: 1,
305
+ InteractiveBook: 2,
306
+ AssessmentSequence: 1
307
+ };
308
+ function getAllowedChildTypes(parent) {
309
+ return ALLOWLISTS[parent];
310
+ }
311
+ function isChildTypeAllowed(parent, childType) {
312
+ return ALLOWLISTS[parent].includes(childType);
313
+ }
314
+ var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
315
+
178
316
  // src/telemetryCatalog.ts
179
317
  var telemetryCatalogVersion = 1;
180
318
  var TELEMETRY_EVENT_CATALOG = [
@@ -247,35 +385,125 @@ function buildTelemetryCatalog() {
247
385
  return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
248
386
  }
249
387
 
250
- // src/trackingClient.ts
388
+ // src/telemetryCatalogV2.ts
389
+ var telemetryCatalogV2Version = 2;
390
+ var TELEMETRY_EVENT_CATALOG_V2 = [
391
+ {
392
+ name: "assessment_answered",
393
+ description: "Learner submitted an assessment interaction answer",
394
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
395
+ dataFields: ["checkId", "interactionType", "question", "response", "correct"],
396
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
397
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
398
+ },
399
+ {
400
+ name: "assessment_completed",
401
+ description: "Assessment interaction completed (passing criteria met)",
402
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
403
+ dataFields: ["checkId", "interactionType", "score", "maxScore", "passingScore"],
404
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
405
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
406
+ }
407
+ ];
408
+ function buildTelemetryCatalogV2() {
409
+ return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
410
+ }
411
+
412
+ // src/telemetryCatalogV3.ts
413
+ var telemetryCatalogV3Version = 3;
414
+ var TELEMETRY_EVENT_CATALOG_V3 = [
415
+ {
416
+ name: "book_page_viewed",
417
+ description: "Learner viewed a page/chapter in an Interactive Book",
418
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
419
+ dataFields: ["blockId", "pageIndex", "pageTitle"],
420
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
421
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
422
+ },
423
+ {
424
+ name: "compound_page_viewed",
425
+ description: "Learner activated a page inside a compound container",
426
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
427
+ dataFields: ["blockId", "pageIndex", "parentType"],
428
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
429
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
430
+ },
431
+ {
432
+ name: "hotspot_opened",
433
+ description: "Learner opened an image hotspot popover",
434
+ requiredFields: ["courseId", "sessionId", "timestamp"],
435
+ dataFields: ["blockId", "hotspotId"],
436
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
437
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
438
+ },
439
+ {
440
+ name: "accordion_section_toggled",
441
+ description: "Learner expanded or collapsed an accordion section",
442
+ requiredFields: ["courseId", "sessionId", "timestamp"],
443
+ dataFields: ["blockId", "sectionId", "expanded"],
444
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
445
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
446
+ },
447
+ {
448
+ name: "flashcard_flipped",
449
+ description: "Learner flipped a flashcard",
450
+ requiredFields: ["courseId", "sessionId", "timestamp"],
451
+ dataFields: ["blockId", "cardIndex", "face"],
452
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
453
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
454
+ },
455
+ {
456
+ name: "image_slider_changed",
457
+ description: "Learner changed the active slide in an image slider",
458
+ requiredFields: ["courseId", "sessionId", "timestamp"],
459
+ dataFields: ["blockId", "slideIndex"],
460
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
461
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
462
+ }
463
+ ];
464
+ function buildTelemetryCatalogV3() {
465
+ return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
466
+ }
467
+
468
+ // src/internal/env.ts
251
469
  function isDevEnvironment() {
252
470
  const g = globalThis;
253
471
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
254
472
  }
473
+ function warnDev(message, err) {
474
+ if (!isDevEnvironment()) return;
475
+ console.warn(message, err instanceof Error ? err.message : err);
476
+ }
477
+
478
+ // src/internal/sinkInvoke.ts
255
479
  function invokeTrackingSink(sink, event) {
256
480
  let result;
257
481
  try {
258
482
  result = sink(event);
259
483
  } catch (err) {
260
- if (isDevEnvironment()) {
261
- console.warn(
262
- "[lessonkit] tracking sink failed:",
263
- err instanceof Error ? err.message : err
264
- );
265
- }
484
+ warnDev("[lessonkit] tracking sink failed:", err);
266
485
  throw err;
267
486
  }
268
487
  if (result != null && typeof result.catch === "function") {
269
- void result.catch((err) => {
270
- if (isDevEnvironment()) {
271
- console.warn(
272
- "[lessonkit] tracking sink failed:",
273
- err instanceof Error ? err.message : err
274
- );
275
- }
276
- });
488
+ void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
277
489
  }
278
490
  }
491
+ function invokePipelineSink(sinkId, emit) {
492
+ let result;
493
+ try {
494
+ result = emit();
495
+ } catch (err) {
496
+ warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
497
+ return;
498
+ }
499
+ if (result != null && typeof result.catch === "function") {
500
+ void result.catch(
501
+ (err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
502
+ );
503
+ }
504
+ }
505
+
506
+ // src/trackingClient.ts
279
507
  function createTrackingClient(opts) {
280
508
  const sink = opts?.sink;
281
509
  const batchSink = opts?.batchSink;
@@ -393,81 +621,234 @@ function nowIso() {
393
621
  return (/* @__PURE__ */ new Date()).toISOString();
394
622
  }
395
623
 
396
- // src/telemetryBuilder.ts
397
- var warnedMissingQuizLesson = false;
398
- function isDevEnvironment2() {
399
- const g = globalThis;
400
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
401
- }
402
- function resetTelemetryBuilderWarningsForTests() {
403
- warnedMissingQuizLesson = false;
404
- }
624
+ // src/telemetry/eventRegistry.ts
405
625
  function resolveLessonId(opts, eventName) {
406
626
  const lessonId = opts.lessonId ?? opts.data?.lessonId;
407
627
  if (!lessonId) throw new Error(`${eventName} requires lessonId`);
408
628
  return lessonId;
409
629
  }
410
- function buildTelemetryEvent(opts) {
411
- const base = {
412
- timestamp: opts.timestamp ?? nowIso(),
413
- courseId: opts.courseId,
414
- sessionId: opts.sessionId,
415
- attemptId: opts.attemptId,
416
- user: opts.user
417
- };
418
- switch (opts.name) {
419
- case "course_started":
420
- return { name: "course_started", ...base };
421
- case "course_completed":
422
- return { name: "course_completed", ...base };
423
- case "lesson_started": {
630
+ function withLessonScopedData(name, base, lessonId, data) {
631
+ return { name, ...base, lessonId, data: { ...data, lessonId } };
632
+ }
633
+ var TELEMETRY_EVENT_REGISTRY = {
634
+ course_started: {
635
+ build: (_opts, base) => ({ name: "course_started", ...base })
636
+ },
637
+ course_completed: {
638
+ build: (_opts, base) => ({ name: "course_completed", ...base })
639
+ },
640
+ lesson_started: {
641
+ requiresLessonId: true,
642
+ build: (opts, base) => {
643
+ if (opts.name !== "lesson_started") throw new Error("unexpected event");
424
644
  const lessonId = resolveLessonId(opts, "lesson_started");
645
+ return withLessonScopedData("lesson_started", base, lessonId, opts.data);
646
+ }
647
+ },
648
+ lesson_completed: {
649
+ requiresLessonId: true,
650
+ build: (opts, base) => {
651
+ if (opts.name !== "lesson_completed") throw new Error("unexpected event");
652
+ const lessonId = resolveLessonId(opts, opts.name);
653
+ return withLessonScopedData(opts.name, base, lessonId, opts.data);
654
+ }
655
+ },
656
+ lesson_time_on_task: {
657
+ requiresLessonId: true,
658
+ build: (opts, base) => {
659
+ if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
660
+ const lessonId = resolveLessonId(opts, opts.name);
661
+ return withLessonScopedData(opts.name, base, lessonId, opts.data);
662
+ }
663
+ },
664
+ quiz_answered: {
665
+ requiresLessonId: true,
666
+ tryBuildMissingLessonWarning: "quiz",
667
+ build: (opts, base) => {
668
+ if (opts.name !== "quiz_answered") throw new Error("unexpected event");
669
+ const lessonId = opts.lessonId;
670
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
425
671
  return {
426
- name: "lesson_started",
672
+ name: "quiz_answered",
427
673
  ...base,
428
674
  lessonId,
429
- data: { ...opts.data, lessonId }
675
+ data: opts.data
430
676
  };
431
677
  }
432
- case "lesson_completed":
433
- case "lesson_time_on_task": {
434
- const lessonId = resolveLessonId(opts, opts.name);
678
+ },
679
+ quiz_completed: {
680
+ requiresLessonId: true,
681
+ tryBuildMissingLessonWarning: "quiz",
682
+ build: (opts, base) => {
683
+ if (opts.name !== "quiz_completed") throw new Error("unexpected event");
684
+ const lessonId = opts.lessonId;
685
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
435
686
  return {
436
- name: opts.name,
687
+ name: "quiz_completed",
437
688
  ...base,
438
689
  lessonId,
439
- data: { ...opts.data, lessonId }
690
+ data: opts.data
440
691
  };
441
692
  }
442
- case "quiz_answered": {
693
+ },
694
+ assessment_answered: {
695
+ requiresLessonId: true,
696
+ tryBuildMissingLessonWarning: "assessment",
697
+ build: (opts, base) => {
698
+ if (opts.name !== "assessment_answered") throw new Error("unexpected event");
443
699
  const lessonId = opts.lessonId;
444
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
445
- return { name: "quiz_answered", ...base, lessonId, data: opts.data };
700
+ if (!lessonId) throw new Error("assessment_answered requires active lessonId");
701
+ return {
702
+ name: "assessment_answered",
703
+ ...base,
704
+ lessonId,
705
+ data: opts.data
706
+ };
446
707
  }
447
- case "quiz_completed": {
708
+ },
709
+ assessment_completed: {
710
+ requiresLessonId: true,
711
+ tryBuildMissingLessonWarning: "assessment",
712
+ build: (opts, base) => {
713
+ if (opts.name !== "assessment_completed") throw new Error("unexpected event");
448
714
  const lessonId = opts.lessonId;
449
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
450
- return { name: "quiz_completed", ...base, lessonId, data: opts.data };
715
+ if (!lessonId) throw new Error("assessment_completed requires active lessonId");
716
+ return {
717
+ name: "assessment_completed",
718
+ ...base,
719
+ lessonId,
720
+ data: opts.data
721
+ };
451
722
  }
452
- case "interaction":
723
+ },
724
+ interaction: {
725
+ build: (opts, base) => {
726
+ if (opts.name !== "interaction") throw new Error("unexpected event");
453
727
  return {
454
728
  name: "interaction",
455
729
  ...base,
456
730
  lessonId: opts.lessonId,
457
731
  data: opts.data
458
732
  };
459
- default:
460
- return assertNever(opts);
733
+ }
734
+ },
735
+ book_page_viewed: {
736
+ requiresLessonId: true,
737
+ build: (opts, base) => {
738
+ if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
739
+ const lessonId = opts.lessonId;
740
+ if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
741
+ return {
742
+ name: "book_page_viewed",
743
+ ...base,
744
+ lessonId,
745
+ data: opts.data
746
+ };
747
+ }
748
+ },
749
+ compound_page_viewed: {
750
+ requiresLessonId: true,
751
+ build: (opts, base) => {
752
+ if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
753
+ const lessonId = opts.lessonId;
754
+ if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
755
+ return {
756
+ name: "compound_page_viewed",
757
+ ...base,
758
+ lessonId,
759
+ data: opts.data
760
+ };
761
+ }
762
+ },
763
+ hotspot_opened: {
764
+ build: (opts, base) => {
765
+ if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
766
+ return {
767
+ name: "hotspot_opened",
768
+ ...base,
769
+ lessonId: opts.lessonId,
770
+ data: opts.data
771
+ };
772
+ }
773
+ },
774
+ accordion_section_toggled: {
775
+ build: (opts, base) => {
776
+ if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
777
+ return {
778
+ name: "accordion_section_toggled",
779
+ ...base,
780
+ lessonId: opts.lessonId,
781
+ data: opts.data
782
+ };
783
+ }
784
+ },
785
+ flashcard_flipped: {
786
+ build: (opts, base) => {
787
+ if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
788
+ return {
789
+ name: "flashcard_flipped",
790
+ ...base,
791
+ lessonId: opts.lessonId,
792
+ data: opts.data
793
+ };
794
+ }
795
+ },
796
+ image_slider_changed: {
797
+ build: (opts, base) => {
798
+ if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
799
+ return {
800
+ name: "image_slider_changed",
801
+ ...base,
802
+ lessonId: opts.lessonId,
803
+ data: opts.data
804
+ };
805
+ }
461
806
  }
807
+ };
808
+ function buildTelemetryEventFromRegistry(opts) {
809
+ const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
810
+ if (!entry) {
811
+ throw new Error("Unexpected value");
812
+ }
813
+ const base = {
814
+ timestamp: opts.timestamp ?? nowIso(),
815
+ courseId: opts.courseId,
816
+ sessionId: opts.sessionId,
817
+ attemptId: opts.attemptId,
818
+ user: opts.user
819
+ };
820
+ return entry.build(opts, base);
821
+ }
822
+ function getTelemetryEventRegistryEntry(name) {
823
+ return TELEMETRY_EVENT_REGISTRY[name];
824
+ }
825
+
826
+ // src/telemetryBuilder.ts
827
+ var warnedMissingQuizLesson = false;
828
+ var warnedMissingAssessmentLesson = false;
829
+ function resetTelemetryBuilderWarningsForTests() {
830
+ warnedMissingQuizLesson = false;
831
+ warnedMissingAssessmentLesson = false;
832
+ }
833
+ function buildTelemetryEvent(opts) {
834
+ return buildTelemetryEventFromRegistry(opts);
462
835
  }
463
836
  function tryBuildTelemetryEvent(opts) {
464
- const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
465
- if (isQuiz && !opts.lessonId) {
466
- if (isDevEnvironment2() && !warnedMissingQuizLesson) {
467
- warnedMissingQuizLesson = true;
468
- console.warn(
469
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
470
- );
837
+ const entry = getTelemetryEventRegistryEntry(opts.name);
838
+ if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
839
+ if (isDevEnvironment()) {
840
+ if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
841
+ warnedMissingQuizLesson = true;
842
+ console.warn(
843
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
844
+ );
845
+ }
846
+ if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
847
+ warnedMissingAssessmentLesson = true;
848
+ console.warn(
849
+ `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
850
+ );
851
+ }
471
852
  }
472
853
  return null;
473
854
  }
@@ -475,29 +856,8 @@ function tryBuildTelemetryEvent(opts) {
475
856
  }
476
857
 
477
858
  // src/telemetryPipeline.ts
478
- function isDevEnvironment3() {
479
- const g = globalThis;
480
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
481
- }
482
- function warnSinkFailure(sinkId, err) {
483
- if (isDevEnvironment3()) {
484
- console.warn(
485
- `[lessonkit] telemetry sink "${sinkId}" failed:`,
486
- err instanceof Error ? err.message : err
487
- );
488
- }
489
- }
490
859
  function invokeSink(sink, event, emitCtx) {
491
- let result;
492
- try {
493
- result = sink.emit(event, emitCtx);
494
- } catch (err) {
495
- warnSinkFailure(sink.id, err);
496
- return;
497
- }
498
- if (result != null && typeof result.catch === "function") {
499
- void result.catch((err) => warnSinkFailure(sink.id, err));
500
- }
860
+ invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
501
861
  }
502
862
  function createTelemetryPipeline(sinks) {
503
863
  const list = [...sinks];
@@ -773,101 +1133,13 @@ function completeCourseWithTelemetry(opts) {
773
1133
  return true;
774
1134
  }
775
1135
 
776
- // src/runtime/createLessonkitRuntime.ts
777
- function createLessonkitRuntime(config, ports = {}) {
778
- const storage = ports.storage ?? createSessionStoragePort();
779
- const clock = ports.clock ?? createDefaultClock();
780
- const configSnapshot = { ...config };
781
- let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
782
- let attemptId = configSnapshot.session?.attemptId;
783
- let user = configSnapshot.session?.user;
784
- let courseId = configSnapshot.courseId;
785
- let progress = createProgressController();
786
- const getSession = () => ({ sessionId, attemptId, user });
787
- const syncSessionFromConfig = (next) => {
788
- sessionId = resolveSessionId(storage, next.session?.sessionId);
789
- attemptId = next.session?.attemptId;
790
- user = next.session?.user;
791
- courseId = next.courseId;
792
- };
793
- syncSessionFromConfig(configSnapshot);
794
- const track = (name, data, emit, lessonId) => {
795
- const event = tryBuildTelemetryEvent({
796
- name,
797
- courseId,
798
- lessonId: lessonId ?? progress.getState().activeLessonId,
799
- sessionId,
800
- attemptId,
801
- user,
802
- data
803
- });
804
- if (!event) return;
805
- emit(event);
806
- };
807
- const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
808
- emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
809
- if (durationMs !== void 0) {
810
- emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
811
- }
812
- };
1136
+ // src/plugins/context.ts
1137
+ function buildPluginContext(opts) {
813
1138
  return {
814
- get config() {
815
- return configSnapshot;
816
- },
817
- get progress() {
818
- return progress;
819
- },
820
- getProgressState: () => progress.getState(),
821
- getSession,
822
- updateConfig(next) {
823
- if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
824
- if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
825
- if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
826
- if (next.session !== void 0) {
827
- configSnapshot.session = { ...configSnapshot.session, ...next.session };
828
- }
829
- syncSessionFromConfig(configSnapshot);
830
- },
831
- setActiveLesson(lessonId, emitFn) {
832
- const current = progress.getState();
833
- if (current.activeLessonId === lessonId) return;
834
- if (current.completedLessonIds.has(lessonId)) {
835
- progress.setActiveLesson(lessonId, clock.nowMs());
836
- return;
837
- }
838
- const previous = current.activeLessonId;
839
- if (previous && previous !== lessonId) {
840
- const completed = progress.completeLesson(previous, clock.nowMs());
841
- if (completed.didComplete) {
842
- emitLessonCompleted(previous, completed.durationMs, emitFn);
843
- }
844
- }
845
- progress.setActiveLesson(lessonId, clock.nowMs());
846
- emitFn("lesson_started", { lessonId }, lessonId);
847
- },
848
- completeLesson(lessonId, emitFn) {
849
- const result = progress.completeLesson(lessonId, clock.nowMs());
850
- if (!result.didComplete) return;
851
- emitLessonCompleted(lessonId, result.durationMs, emitFn);
852
- },
853
- completeCourse(emitFn) {
854
- const current = progress.getState();
855
- if (current.activeLessonId) {
856
- const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
857
- if (lessonResult.didComplete) {
858
- emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
859
- }
860
- }
861
- const result = progress.completeCourse();
862
- if (!result.didComplete) return;
863
- emitFn("course_completed");
864
- },
865
- track,
866
- resetForCourseChange(nextCourseId) {
867
- configSnapshot.courseId = nextCourseId;
868
- courseId = nextCourseId;
869
- progress = createProgressController();
870
- }
1139
+ courseId: opts.courseId,
1140
+ sessionId: opts.sessionId,
1141
+ attemptId: opts.attemptId,
1142
+ user: opts.user
871
1143
  };
872
1144
  }
873
1145
 
@@ -958,6 +1230,161 @@ function createPluginRegistry(plugins = []) {
958
1230
  };
959
1231
  }
960
1232
 
1233
+ // src/runtime/createLessonkitRuntime.ts
1234
+ function resolvePluginHost(plugins) {
1235
+ if (!plugins) return null;
1236
+ if (typeof plugins === "object" && "runTelemetry" in plugins) return plugins;
1237
+ if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
1238
+ return null;
1239
+ }
1240
+ function warnRuntimeV1Deprecated() {
1241
+ const g = globalThis;
1242
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
1243
+ console.warn(
1244
+ '[lessonkit] runtimeVersion "v1" is deprecated; use "v2" (default). v1 will be removed in LessonKit 2.0.'
1245
+ );
1246
+ }
1247
+ function createLessonkitRuntime(config, ports = {}) {
1248
+ if (config.runtimeVersion === "v1") warnRuntimeV1Deprecated();
1249
+ const storage = ports.storage ?? createSessionStoragePort();
1250
+ const clock = ports.clock ?? createDefaultClock();
1251
+ const configSnapshot = { ...config };
1252
+ let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
1253
+ let attemptId = configSnapshot.session?.attemptId;
1254
+ let user = configSnapshot.session?.user;
1255
+ let courseId = configSnapshot.courseId;
1256
+ let progress = createProgressController();
1257
+ let pluginHost = resolvePluginHost(configSnapshot.plugins);
1258
+ const getPluginCtx = () => buildPluginContext({
1259
+ courseId,
1260
+ sessionId,
1261
+ attemptId,
1262
+ user
1263
+ });
1264
+ const getSession = () => ({ sessionId, attemptId, user });
1265
+ const syncSessionFromConfig = (next) => {
1266
+ sessionId = resolveSessionId(storage, next.session?.sessionId);
1267
+ attemptId = next.session?.attemptId;
1268
+ user = next.session?.user;
1269
+ courseId = next.courseId;
1270
+ };
1271
+ const applyPluginsToEvent = (event) => {
1272
+ if (!pluginHost) return event;
1273
+ return pluginHost.runTelemetry(event, getPluginCtx());
1274
+ };
1275
+ const buildAndApply = (name, data, lessonId) => {
1276
+ const event = tryBuildTelemetryEvent({
1277
+ name,
1278
+ courseId,
1279
+ lessonId: lessonId ?? progress.getState().activeLessonId,
1280
+ sessionId,
1281
+ attemptId,
1282
+ user,
1283
+ data
1284
+ });
1285
+ if (!event) return null;
1286
+ return applyPluginsToEvent(event);
1287
+ };
1288
+ const wrapEmitFn = (emitFn) => {
1289
+ return (name, data, lessonId) => {
1290
+ const event = buildAndApply(name, data, lessonId);
1291
+ if (event === null) return;
1292
+ const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
1293
+ const eventData = "data" in event ? event.data : data;
1294
+ emitFn(event.name, eventData, eventLessonId);
1295
+ };
1296
+ };
1297
+ syncSessionFromConfig(configSnapshot);
1298
+ const track = (name, data, emit, lessonId) => {
1299
+ const event = buildAndApply(name, data, lessonId);
1300
+ if (!event) return;
1301
+ emit(event);
1302
+ };
1303
+ const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
1304
+ const wrapped = wrapEmitFn(emitFn);
1305
+ wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
1306
+ if (durationMs !== void 0) {
1307
+ wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
1308
+ }
1309
+ };
1310
+ return {
1311
+ get config() {
1312
+ return configSnapshot;
1313
+ },
1314
+ get progress() {
1315
+ return progress;
1316
+ },
1317
+ get pluginHost() {
1318
+ return pluginHost;
1319
+ },
1320
+ getProgressState: () => progress.getState(),
1321
+ getSession,
1322
+ updateConfig(next) {
1323
+ if (next.plugins !== void 0 && next.plugins !== pluginHost) {
1324
+ pluginHost?.disposeAll();
1325
+ configSnapshot.plugins = next.plugins;
1326
+ pluginHost = resolvePluginHost(configSnapshot.plugins);
1327
+ }
1328
+ if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
1329
+ if (next.runtimeVersion !== void 0) {
1330
+ if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
1331
+ configSnapshot.runtimeVersion = next.runtimeVersion;
1332
+ }
1333
+ if (next.session !== void 0) {
1334
+ configSnapshot.session = { ...configSnapshot.session, ...next.session };
1335
+ }
1336
+ syncSessionFromConfig(configSnapshot);
1337
+ },
1338
+ setActiveLesson(lessonId, emitFn) {
1339
+ const wrapped = wrapEmitFn(emitFn);
1340
+ const current = progress.getState();
1341
+ if (current.activeLessonId === lessonId) return;
1342
+ if (current.completedLessonIds.has(lessonId)) {
1343
+ progress.setActiveLesson(lessonId, clock.nowMs());
1344
+ return;
1345
+ }
1346
+ const previous = current.activeLessonId;
1347
+ if (previous && previous !== lessonId) {
1348
+ const completed = progress.completeLesson(previous, clock.nowMs());
1349
+ if (completed.didComplete) {
1350
+ emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
1351
+ }
1352
+ }
1353
+ progress.setActiveLesson(lessonId, clock.nowMs());
1354
+ wrapped("lesson_started", { lessonId }, lessonId);
1355
+ },
1356
+ completeLesson(lessonId, emitFn) {
1357
+ completeLessonWithTelemetry({
1358
+ progress,
1359
+ lessonId,
1360
+ nowMs: clock.nowMs(),
1361
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
1362
+ });
1363
+ },
1364
+ completeCourse(emitFn) {
1365
+ completeCourseWithTelemetry({
1366
+ progress,
1367
+ nowMs: clock.nowMs(),
1368
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
1369
+ emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
1370
+ });
1371
+ },
1372
+ track,
1373
+ scoreAssessment(input, _lessonId) {
1374
+ if (!pluginHost) return null;
1375
+ return pluginHost.scoreAssessment(input, getPluginCtx());
1376
+ },
1377
+ resetForCourseChange(nextCourseId) {
1378
+ configSnapshot.courseId = nextCourseId;
1379
+ courseId = nextCourseId;
1380
+ progress = createProgressController();
1381
+ },
1382
+ dispose() {
1383
+ pluginHost?.disposeAll();
1384
+ }
1385
+ };
1386
+ }
1387
+
961
1388
  // src/plugins/define.ts
962
1389
  function defineTelemetryPlugin(plugin) {
963
1390
  return plugin;
@@ -970,18 +1397,33 @@ function defineLifecyclePlugin(plugin) {
970
1397
  }
971
1398
  // Annotate the CommonJS export names for ESM import in node:
972
1399
  0 && (module.exports = {
1400
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
1401
+ ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
1402
+ COMPOUND_MAX_NESTING_DEPTH,
1403
+ COMPOUND_RESUME_SCHEMA_VERSION,
973
1404
  ID_MAX_LENGTH,
974
1405
  ID_PATTERN,
1406
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1407
+ PAGE_ALLOWED_CHILD_TYPES,
975
1408
  SESSION_STORAGE_KEY,
976
1409
  TELEMETRY_EVENT_CATALOG,
1410
+ TELEMETRY_EVENT_CATALOG_V2,
1411
+ TELEMETRY_EVENT_CATALOG_V3,
977
1412
  assertNever,
978
1413
  assertValidId,
979
1414
  buildCourseStartedTelemetryEvent,
980
1415
  buildLessonkitUrn,
1416
+ buildPluginContext,
981
1417
  buildTelemetryCatalog,
1418
+ buildTelemetryCatalogV2,
1419
+ buildTelemetryCatalogV3,
982
1420
  buildTelemetryEvent,
1421
+ clampCompoundPageIndex,
1422
+ clearCompoundState,
983
1423
  completeCourseWithTelemetry,
984
1424
  completeLessonWithTelemetry,
1425
+ compoundStateStorageKey,
1426
+ createCompoundResumeState,
985
1427
  createDefaultClock,
986
1428
  createGlobalTimer,
987
1429
  createLessonkitRuntime,
@@ -997,10 +1439,13 @@ function defineLifecyclePlugin(plugin) {
997
1439
  defineLifecyclePlugin,
998
1440
  defineTelemetryPlugin,
999
1441
  deriveId,
1442
+ getAllowedChildTypes,
1000
1443
  getTabSessionId,
1001
1444
  hasCourseStarted,
1002
1445
  hasCourseStartedEmittedToTracking,
1003
1446
  hasCourseStartedPipelineDelivered,
1447
+ isChildTypeAllowed,
1448
+ loadCompoundState,
1004
1449
  markCourseStarted,
1005
1450
  markCourseStartedEmittedToTracking,
1006
1451
  markCourseStartedPipelineDelivered,
@@ -1008,12 +1453,16 @@ function defineLifecyclePlugin(plugin) {
1008
1453
  nowIso,
1009
1454
  parseBlockId,
1010
1455
  parseCheckId,
1456
+ parseCompoundResumeState,
1011
1457
  parseCourseId,
1012
1458
  parseLessonId,
1013
1459
  resetStoragePortForTests,
1014
1460
  resetTelemetryBuilderWarningsForTests,
1015
1461
  resolveSessionId,
1462
+ saveCompoundState,
1016
1463
  slugifyId,
1464
+ telemetryCatalogV2Version,
1465
+ telemetryCatalogV3Version,
1017
1466
  telemetryCatalogVersion,
1018
1467
  tryBuildTelemetryEvent,
1019
1468
  tryEmitCourseStarted,