@lessonkit/core 1.1.0 → 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,20 +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,
27
33
  TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
34
+ TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
28
35
  assertNever: () => assertNever,
29
36
  assertValidId: () => assertValidId,
30
37
  buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
31
38
  buildLessonkitUrn: () => buildLessonkitUrn,
39
+ buildPluginContext: () => buildPluginContext,
32
40
  buildTelemetryCatalog: () => buildTelemetryCatalog,
33
41
  buildTelemetryCatalogV2: () => buildTelemetryCatalogV2,
42
+ buildTelemetryCatalogV3: () => buildTelemetryCatalogV3,
34
43
  buildTelemetryEvent: () => buildTelemetryEvent,
44
+ clampCompoundPageIndex: () => clampCompoundPageIndex,
45
+ clearCompoundState: () => clearCompoundState,
35
46
  completeCourseWithTelemetry: () => completeCourseWithTelemetry,
36
47
  completeLessonWithTelemetry: () => completeLessonWithTelemetry,
48
+ compoundStateStorageKey: () => compoundStateStorageKey,
49
+ createCompoundResumeState: () => createCompoundResumeState,
37
50
  createDefaultClock: () => createDefaultClock,
38
51
  createGlobalTimer: () => createGlobalTimer,
39
52
  createLessonkitRuntime: () => createLessonkitRuntime,
@@ -49,10 +62,13 @@ __export(index_exports, {
49
62
  defineLifecyclePlugin: () => defineLifecyclePlugin,
50
63
  defineTelemetryPlugin: () => defineTelemetryPlugin,
51
64
  deriveId: () => deriveId,
65
+ getAllowedChildTypes: () => getAllowedChildTypes,
52
66
  getTabSessionId: () => getTabSessionId,
53
67
  hasCourseStarted: () => hasCourseStarted,
54
68
  hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
55
69
  hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
70
+ isChildTypeAllowed: () => isChildTypeAllowed,
71
+ loadCompoundState: () => loadCompoundState,
56
72
  markCourseStarted: () => markCourseStarted,
57
73
  markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
58
74
  markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
@@ -60,13 +76,16 @@ __export(index_exports, {
60
76
  nowIso: () => nowIso,
61
77
  parseBlockId: () => parseBlockId,
62
78
  parseCheckId: () => parseCheckId,
79
+ parseCompoundResumeState: () => parseCompoundResumeState,
63
80
  parseCourseId: () => parseCourseId,
64
81
  parseLessonId: () => parseLessonId,
65
82
  resetStoragePortForTests: () => resetStoragePortForTests,
66
83
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
67
84
  resolveSessionId: () => resolveSessionId,
85
+ saveCompoundState: () => saveCompoundState,
68
86
  slugifyId: () => slugifyId,
69
87
  telemetryCatalogV2Version: () => telemetryCatalogV2Version,
88
+ telemetryCatalogV3Version: () => telemetryCatalogV3Version,
70
89
  telemetryCatalogVersion: () => telemetryCatalogVersion,
71
90
  tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
72
91
  tryEmitCourseStarted: () => tryEmitCourseStarted,
@@ -178,6 +197,122 @@ function buildLessonkitUrn(parts) {
178
197
  return urn;
179
198
  }
180
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
+
181
316
  // src/telemetryCatalog.ts
182
317
  var telemetryCatalogVersion = 1;
183
318
  var TELEMETRY_EVENT_CATALOG = [
@@ -274,35 +409,101 @@ function buildTelemetryCatalogV2() {
274
409
  return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
275
410
  }
276
411
 
277
- // src/trackingClient.ts
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
278
469
  function isDevEnvironment() {
279
470
  const g = globalThis;
280
471
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
281
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
282
479
  function invokeTrackingSink(sink, event) {
283
480
  let result;
284
481
  try {
285
482
  result = sink(event);
286
483
  } catch (err) {
287
- if (isDevEnvironment()) {
288
- console.warn(
289
- "[lessonkit] tracking sink failed:",
290
- err instanceof Error ? err.message : err
291
- );
292
- }
484
+ warnDev("[lessonkit] tracking sink failed:", err);
293
485
  throw err;
294
486
  }
295
487
  if (result != null && typeof result.catch === "function") {
296
- void result.catch((err) => {
297
- if (isDevEnvironment()) {
298
- console.warn(
299
- "[lessonkit] tracking sink failed:",
300
- err instanceof Error ? err.message : err
301
- );
302
- }
303
- });
488
+ void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
304
489
  }
305
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
306
507
  function createTrackingClient(opts) {
307
508
  const sink = opts?.sink;
308
509
  const batchSink = opts?.batchSink;
@@ -420,96 +621,229 @@ function nowIso() {
420
621
  return (/* @__PURE__ */ new Date()).toISOString();
421
622
  }
422
623
 
423
- // src/telemetryBuilder.ts
424
- var warnedMissingQuizLesson = false;
425
- var warnedMissingAssessmentLesson = false;
426
- function isDevEnvironment2() {
427
- const g = globalThis;
428
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
429
- }
430
- function resetTelemetryBuilderWarningsForTests() {
431
- warnedMissingQuizLesson = false;
432
- warnedMissingAssessmentLesson = false;
433
- }
624
+ // src/telemetry/eventRegistry.ts
434
625
  function resolveLessonId(opts, eventName) {
435
626
  const lessonId = opts.lessonId ?? opts.data?.lessonId;
436
627
  if (!lessonId) throw new Error(`${eventName} requires lessonId`);
437
628
  return lessonId;
438
629
  }
439
- function buildTelemetryEvent(opts) {
440
- const base = {
441
- timestamp: opts.timestamp ?? nowIso(),
442
- courseId: opts.courseId,
443
- sessionId: opts.sessionId,
444
- attemptId: opts.attemptId,
445
- user: opts.user
446
- };
447
- switch (opts.name) {
448
- case "course_started":
449
- return { name: "course_started", ...base };
450
- case "course_completed":
451
- return { name: "course_completed", ...base };
452
- 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");
453
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");
454
671
  return {
455
- name: "lesson_started",
672
+ name: "quiz_answered",
456
673
  ...base,
457
674
  lessonId,
458
- data: { ...opts.data, lessonId }
675
+ data: opts.data
459
676
  };
460
677
  }
461
- case "lesson_completed":
462
- case "lesson_time_on_task": {
463
- 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");
464
686
  return {
465
- name: opts.name,
687
+ name: "quiz_completed",
466
688
  ...base,
467
689
  lessonId,
468
- data: { ...opts.data, lessonId }
690
+ data: opts.data
469
691
  };
470
692
  }
471
- 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");
472
699
  const lessonId = opts.lessonId;
473
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
474
- 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
+ };
475
707
  }
476
- 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");
477
714
  const lessonId = opts.lessonId;
478
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
479
- 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
+ };
722
+ }
723
+ },
724
+ interaction: {
725
+ build: (opts, base) => {
726
+ if (opts.name !== "interaction") throw new Error("unexpected event");
727
+ return {
728
+ name: "interaction",
729
+ ...base,
730
+ lessonId: opts.lessonId,
731
+ data: opts.data
732
+ };
480
733
  }
481
- case "assessment_answered": {
734
+ },
735
+ book_page_viewed: {
736
+ requiresLessonId: true,
737
+ build: (opts, base) => {
738
+ if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
482
739
  const lessonId = opts.lessonId;
483
- if (!lessonId) throw new Error("assessment_answered requires active lessonId");
484
- return { name: "assessment_answered", ...base, lessonId, data: opts.data };
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
+ };
485
747
  }
486
- case "assessment_completed": {
748
+ },
749
+ compound_page_viewed: {
750
+ requiresLessonId: true,
751
+ build: (opts, base) => {
752
+ if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
487
753
  const lessonId = opts.lessonId;
488
- if (!lessonId) throw new Error("assessment_completed requires active lessonId");
489
- return { name: "assessment_completed", ...base, lessonId, data: opts.data };
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
+ };
490
761
  }
491
- case "interaction":
762
+ },
763
+ hotspot_opened: {
764
+ build: (opts, base) => {
765
+ if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
492
766
  return {
493
- name: "interaction",
767
+ name: "hotspot_opened",
494
768
  ...base,
495
769
  lessonId: opts.lessonId,
496
770
  data: opts.data
497
771
  };
498
- default:
499
- return assertNever(opts);
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
+ }
806
+ }
807
+ };
808
+ function buildTelemetryEventFromRegistry(opts) {
809
+ const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
810
+ if (!entry) {
811
+ throw new Error("Unexpected value");
500
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);
501
835
  }
502
836
  function tryBuildTelemetryEvent(opts) {
503
- const needsLesson = opts.name === "quiz_answered" || opts.name === "quiz_completed" || opts.name === "assessment_answered" || opts.name === "assessment_completed";
504
- if (needsLesson && !opts.lessonId) {
505
- if (isDevEnvironment2()) {
506
- if (opts.name.startsWith("quiz_") && !warnedMissingQuizLesson) {
837
+ const entry = getTelemetryEventRegistryEntry(opts.name);
838
+ if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
839
+ if (isDevEnvironment()) {
840
+ if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
507
841
  warnedMissingQuizLesson = true;
508
842
  console.warn(
509
843
  `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
510
844
  );
511
845
  }
512
- if (opts.name.startsWith("assessment_") && !warnedMissingAssessmentLesson) {
846
+ if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
513
847
  warnedMissingAssessmentLesson = true;
514
848
  console.warn(
515
849
  `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
@@ -522,29 +856,8 @@ function tryBuildTelemetryEvent(opts) {
522
856
  }
523
857
 
524
858
  // src/telemetryPipeline.ts
525
- function isDevEnvironment3() {
526
- const g = globalThis;
527
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
528
- }
529
- function warnSinkFailure(sinkId, err) {
530
- if (isDevEnvironment3()) {
531
- console.warn(
532
- `[lessonkit] telemetry sink "${sinkId}" failed:`,
533
- err instanceof Error ? err.message : err
534
- );
535
- }
536
- }
537
859
  function invokeSink(sink, event, emitCtx) {
538
- let result;
539
- try {
540
- result = sink.emit(event, emitCtx);
541
- } catch (err) {
542
- warnSinkFailure(sink.id, err);
543
- return;
544
- }
545
- if (result != null && typeof result.catch === "function") {
546
- void result.catch((err) => warnSinkFailure(sink.id, err));
547
- }
860
+ invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
548
861
  }
549
862
  function createTelemetryPipeline(sinks) {
550
863
  const list = [...sinks];
@@ -820,101 +1133,13 @@ function completeCourseWithTelemetry(opts) {
820
1133
  return true;
821
1134
  }
822
1135
 
823
- // src/runtime/createLessonkitRuntime.ts
824
- function createLessonkitRuntime(config, ports = {}) {
825
- const storage = ports.storage ?? createSessionStoragePort();
826
- const clock = ports.clock ?? createDefaultClock();
827
- const configSnapshot = { ...config };
828
- let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
829
- let attemptId = configSnapshot.session?.attemptId;
830
- let user = configSnapshot.session?.user;
831
- let courseId = configSnapshot.courseId;
832
- let progress = createProgressController();
833
- const getSession = () => ({ sessionId, attemptId, user });
834
- const syncSessionFromConfig = (next) => {
835
- sessionId = resolveSessionId(storage, next.session?.sessionId);
836
- attemptId = next.session?.attemptId;
837
- user = next.session?.user;
838
- courseId = next.courseId;
839
- };
840
- syncSessionFromConfig(configSnapshot);
841
- const track = (name, data, emit, lessonId) => {
842
- const event = tryBuildTelemetryEvent({
843
- name,
844
- courseId,
845
- lessonId: lessonId ?? progress.getState().activeLessonId,
846
- sessionId,
847
- attemptId,
848
- user,
849
- data
850
- });
851
- if (!event) return;
852
- emit(event);
853
- };
854
- const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
855
- emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
856
- if (durationMs !== void 0) {
857
- emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
858
- }
859
- };
1136
+ // src/plugins/context.ts
1137
+ function buildPluginContext(opts) {
860
1138
  return {
861
- get config() {
862
- return configSnapshot;
863
- },
864
- get progress() {
865
- return progress;
866
- },
867
- getProgressState: () => progress.getState(),
868
- getSession,
869
- updateConfig(next) {
870
- if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
871
- if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
872
- if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
873
- if (next.session !== void 0) {
874
- configSnapshot.session = { ...configSnapshot.session, ...next.session };
875
- }
876
- syncSessionFromConfig(configSnapshot);
877
- },
878
- setActiveLesson(lessonId, emitFn) {
879
- const current = progress.getState();
880
- if (current.activeLessonId === lessonId) return;
881
- if (current.completedLessonIds.has(lessonId)) {
882
- progress.setActiveLesson(lessonId, clock.nowMs());
883
- return;
884
- }
885
- const previous = current.activeLessonId;
886
- if (previous && previous !== lessonId) {
887
- const completed = progress.completeLesson(previous, clock.nowMs());
888
- if (completed.didComplete) {
889
- emitLessonCompleted(previous, completed.durationMs, emitFn);
890
- }
891
- }
892
- progress.setActiveLesson(lessonId, clock.nowMs());
893
- emitFn("lesson_started", { lessonId }, lessonId);
894
- },
895
- completeLesson(lessonId, emitFn) {
896
- const result = progress.completeLesson(lessonId, clock.nowMs());
897
- if (!result.didComplete) return;
898
- emitLessonCompleted(lessonId, result.durationMs, emitFn);
899
- },
900
- completeCourse(emitFn) {
901
- const current = progress.getState();
902
- if (current.activeLessonId) {
903
- const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
904
- if (lessonResult.didComplete) {
905
- emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
906
- }
907
- }
908
- const result = progress.completeCourse();
909
- if (!result.didComplete) return;
910
- emitFn("course_completed");
911
- },
912
- track,
913
- resetForCourseChange(nextCourseId) {
914
- configSnapshot.courseId = nextCourseId;
915
- courseId = nextCourseId;
916
- progress = createProgressController();
917
- }
1139
+ courseId: opts.courseId,
1140
+ sessionId: opts.sessionId,
1141
+ attemptId: opts.attemptId,
1142
+ user: opts.user
918
1143
  };
919
1144
  }
920
1145
 
@@ -1005,6 +1230,161 @@ function createPluginRegistry(plugins = []) {
1005
1230
  };
1006
1231
  }
1007
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
+
1008
1388
  // src/plugins/define.ts
1009
1389
  function defineTelemetryPlugin(plugin) {
1010
1390
  return plugin;
@@ -1017,20 +1397,33 @@ function defineLifecyclePlugin(plugin) {
1017
1397
  }
1018
1398
  // Annotate the CommonJS export names for ESM import in node:
1019
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,
1020
1404
  ID_MAX_LENGTH,
1021
1405
  ID_PATTERN,
1406
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1407
+ PAGE_ALLOWED_CHILD_TYPES,
1022
1408
  SESSION_STORAGE_KEY,
1023
1409
  TELEMETRY_EVENT_CATALOG,
1024
1410
  TELEMETRY_EVENT_CATALOG_V2,
1411
+ TELEMETRY_EVENT_CATALOG_V3,
1025
1412
  assertNever,
1026
1413
  assertValidId,
1027
1414
  buildCourseStartedTelemetryEvent,
1028
1415
  buildLessonkitUrn,
1416
+ buildPluginContext,
1029
1417
  buildTelemetryCatalog,
1030
1418
  buildTelemetryCatalogV2,
1419
+ buildTelemetryCatalogV3,
1031
1420
  buildTelemetryEvent,
1421
+ clampCompoundPageIndex,
1422
+ clearCompoundState,
1032
1423
  completeCourseWithTelemetry,
1033
1424
  completeLessonWithTelemetry,
1425
+ compoundStateStorageKey,
1426
+ createCompoundResumeState,
1034
1427
  createDefaultClock,
1035
1428
  createGlobalTimer,
1036
1429
  createLessonkitRuntime,
@@ -1046,10 +1439,13 @@ function defineLifecyclePlugin(plugin) {
1046
1439
  defineLifecyclePlugin,
1047
1440
  defineTelemetryPlugin,
1048
1441
  deriveId,
1442
+ getAllowedChildTypes,
1049
1443
  getTabSessionId,
1050
1444
  hasCourseStarted,
1051
1445
  hasCourseStartedEmittedToTracking,
1052
1446
  hasCourseStartedPipelineDelivered,
1447
+ isChildTypeAllowed,
1448
+ loadCompoundState,
1053
1449
  markCourseStarted,
1054
1450
  markCourseStartedEmittedToTracking,
1055
1451
  markCourseStartedPipelineDelivered,
@@ -1057,13 +1453,16 @@ function defineLifecyclePlugin(plugin) {
1057
1453
  nowIso,
1058
1454
  parseBlockId,
1059
1455
  parseCheckId,
1456
+ parseCompoundResumeState,
1060
1457
  parseCourseId,
1061
1458
  parseLessonId,
1062
1459
  resetStoragePortForTests,
1063
1460
  resetTelemetryBuilderWarningsForTests,
1064
1461
  resolveSessionId,
1462
+ saveCompoundState,
1065
1463
  slugifyId,
1066
1464
  telemetryCatalogV2Version,
1465
+ telemetryCatalogV3Version,
1067
1466
  telemetryCatalogVersion,
1068
1467
  tryBuildTelemetryEvent,
1069
1468
  tryEmitCourseStarted,