@lessonkit/core 1.0.0 → 1.0.1

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
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  ID_PATTERN: () => ID_PATTERN,
25
25
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
26
26
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
27
+ assertNever: () => assertNever,
27
28
  assertValidId: () => assertValidId,
28
29
  buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
29
30
  buildLessonkitUrn: () => buildLessonkitUrn,
@@ -49,10 +50,16 @@ __export(index_exports, {
49
50
  getTabSessionId: () => getTabSessionId,
50
51
  hasCourseStarted: () => hasCourseStarted,
51
52
  hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
53
+ hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
52
54
  markCourseStarted: () => markCourseStarted,
53
55
  markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
56
+ markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
54
57
  migrateCourseStartedMark: () => migrateCourseStartedMark,
55
58
  nowIso: () => nowIso,
59
+ parseBlockId: () => parseBlockId,
60
+ parseCheckId: () => parseCheckId,
61
+ parseCourseId: () => parseCourseId,
62
+ parseLessonId: () => parseLessonId,
56
63
  resetStoragePortForTests: () => resetStoragePortForTests,
57
64
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
58
65
  resolveSessionId: () => resolveSessionId,
@@ -68,6 +75,11 @@ module.exports = __toCommonJS(index_exports);
68
75
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
69
76
  var ID_MAX_LENGTH = 64;
70
77
 
78
+ // src/assertNever.ts
79
+ function assertNever(value, message = "Unexpected value") {
80
+ throw new Error(`${message}: ${String(value)}`);
81
+ }
82
+
71
83
  // src/validateId.ts
72
84
  function validateId(input, path = "id") {
73
85
  if (typeof input !== "string") {
@@ -96,6 +108,22 @@ function validateId(input, path = "id") {
96
108
  }
97
109
  return { ok: true, id };
98
110
  }
111
+ function parseCourseId(input) {
112
+ const result = validateId(input, "courseId");
113
+ return result.ok ? result.id : null;
114
+ }
115
+ function parseLessonId(input) {
116
+ const result = validateId(input, "lessonId");
117
+ return result.ok ? result.id : null;
118
+ }
119
+ function parseCheckId(input) {
120
+ const result = validateId(input, "checkId");
121
+ return result.ok ? result.id : null;
122
+ }
123
+ function parseBlockId(input) {
124
+ const result = validateId(input, "blockId");
125
+ return result.ok ? result.id : null;
126
+ }
99
127
  function assertValidId(input, path = "id") {
100
128
  const result = validateId(input, path);
101
129
  if (!result.ok) {
@@ -369,6 +397,11 @@ function isDevEnvironment2() {
369
397
  function resetTelemetryBuilderWarningsForTests() {
370
398
  warnedMissingQuizLesson = false;
371
399
  }
400
+ function resolveLessonId(opts, eventName) {
401
+ const lessonId = opts.lessonId ?? opts.data?.lessonId;
402
+ if (!lessonId) throw new Error(`${eventName} requires lessonId`);
403
+ return lessonId;
404
+ }
372
405
  function buildTelemetryEvent(opts) {
373
406
  const base = {
374
407
  timestamp: opts.timestamp ?? nowIso(),
@@ -383,39 +416,33 @@ function buildTelemetryEvent(opts) {
383
416
  case "course_completed":
384
417
  return { name: "course_completed", ...base };
385
418
  case "lesson_started": {
386
- const data = opts.data;
387
- const lessonId = opts.lessonId ?? data?.lessonId;
388
- if (!lessonId) throw new Error("lesson_started requires lessonId");
419
+ const lessonId = resolveLessonId(opts, "lesson_started");
389
420
  return {
390
421
  name: "lesson_started",
391
422
  ...base,
392
423
  lessonId,
393
- data: { ...data, lessonId }
424
+ data: { ...opts.data, lessonId }
394
425
  };
395
426
  }
396
427
  case "lesson_completed":
397
428
  case "lesson_time_on_task": {
398
- const data = opts.data;
399
- const lessonId = opts.lessonId ?? data?.lessonId;
400
- if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
429
+ const lessonId = resolveLessonId(opts, opts.name);
401
430
  return {
402
431
  name: opts.name,
403
432
  ...base,
404
433
  lessonId,
405
- data: { ...data, lessonId }
434
+ data: { ...opts.data, lessonId }
406
435
  };
407
436
  }
408
437
  case "quiz_answered": {
409
- const data = opts.data;
410
438
  const lessonId = opts.lessonId;
411
439
  if (!lessonId) throw new Error("quiz_answered requires active lessonId");
412
- return { name: "quiz_answered", ...base, lessonId, data };
440
+ return { name: "quiz_answered", ...base, lessonId, data: opts.data };
413
441
  }
414
442
  case "quiz_completed": {
415
- const data = opts.data;
416
443
  const lessonId = opts.lessonId;
417
444
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
418
- return { name: "quiz_completed", ...base, lessonId, data };
445
+ return { name: "quiz_completed", ...base, lessonId, data: opts.data };
419
446
  }
420
447
  case "interaction":
421
448
  return {
@@ -425,7 +452,7 @@ function buildTelemetryEvent(opts) {
425
452
  data: opts.data
426
453
  };
427
454
  default:
428
- return { name: opts.name, ...base };
455
+ return assertNever(opts);
429
456
  }
430
457
  }
431
458
  function tryBuildTelemetryEvent(opts) {
@@ -622,6 +649,7 @@ function getTabSessionId(storage) {
622
649
  }
623
650
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
624
651
  var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
652
+ var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
625
653
  function resolveSessionId(storage, provided) {
626
654
  if (provided) return provided;
627
655
  const existing = storage.getItem(SESSION_STORAGE_KEY);
@@ -636,6 +664,9 @@ function courseStartedStorageKey(sessionId, courseId) {
636
664
  function courseStartedTrackingStorageKey(sessionId, courseId) {
637
665
  return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
638
666
  }
667
+ function courseStartedPipelineStorageKey(sessionId, courseId) {
668
+ return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
669
+ }
639
670
  function hasCourseStarted(storage, sessionId, courseId) {
640
671
  if (!courseId) return false;
641
672
  return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
@@ -652,6 +683,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
652
683
  if (!courseId) return;
653
684
  storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
654
685
  }
686
+ function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
687
+ if (!courseId) return false;
688
+ return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
689
+ }
690
+ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
691
+ if (!courseId) return;
692
+ storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
693
+ }
655
694
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
656
695
  if (!courseId || fromSessionId === toSessionId) return;
657
696
  if (hasCourseStarted(storage, fromSessionId, courseId)) {
@@ -662,6 +701,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
662
701
  markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
663
702
  storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
664
703
  }
704
+ if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
705
+ markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
706
+ storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
707
+ }
665
708
  }
666
709
 
667
710
  // src/runtime/courseLifecycle.ts
@@ -911,6 +954,7 @@ function defineLifecyclePlugin(plugin) {
911
954
  ID_PATTERN,
912
955
  SESSION_STORAGE_KEY,
913
956
  TELEMETRY_EVENT_CATALOG,
957
+ assertNever,
914
958
  assertValidId,
915
959
  buildCourseStartedTelemetryEvent,
916
960
  buildLessonkitUrn,
@@ -936,10 +980,16 @@ function defineLifecyclePlugin(plugin) {
936
980
  getTabSessionId,
937
981
  hasCourseStarted,
938
982
  hasCourseStartedEmittedToTracking,
983
+ hasCourseStartedPipelineDelivered,
939
984
  markCourseStarted,
940
985
  markCourseStartedEmittedToTracking,
986
+ markCourseStartedPipelineDelivered,
941
987
  migrateCourseStartedMark,
942
988
  nowIso,
989
+ parseBlockId,
990
+ parseCheckId,
991
+ parseCourseId,
992
+ parseLessonId,
943
993
  resetStoragePortForTests,
944
994
  resetTelemetryBuilderWarningsForTests,
945
995
  resolveSessionId,
package/dist/index.d.cts CHANGED
@@ -2,6 +2,8 @@ type CourseId = string;
2
2
  type LessonId = string;
3
3
  type CheckId = string;
4
4
  type BlockId = string;
5
+ /** Stable URN string returned by {@link buildLessonkitUrn}. */
6
+ type LessonkitUrn = string;
5
7
  type IdentityValidationIssue = {
6
8
  path: string;
7
9
  message: string;
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
13
15
  ok: false;
14
16
  issues: IdentityValidationIssue[];
15
17
  };
18
+ type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
16
19
  /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
20
  declare const ID_PATTERN: RegExp;
18
21
  declare const ID_MAX_LENGTH = 64;
19
22
 
20
- declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
- declare function assertValidId(input: unknown, path?: string): string;
23
+ /**
24
+ * Exhaustiveness helper for switch/default branches.
25
+ * @throws when called at runtime with an unexpected value.
26
+ */
27
+ declare function assertNever(value: never, message?: string): never;
28
+
29
+ declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
30
+ declare function parseCourseId(input: unknown): CourseId | null;
31
+ declare function parseLessonId(input: unknown): LessonId | null;
32
+ declare function parseCheckId(input: unknown): CheckId | null;
33
+ declare function parseBlockId(input: unknown): BlockId | null;
34
+ declare function assertValidId(input: unknown, path: "courseId"): CourseId;
35
+ declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
36
+ declare function assertValidId(input: unknown, path: "checkId"): CheckId;
37
+ declare function assertValidId(input: unknown, path: "blockId"): BlockId;
38
+ declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
22
39
 
23
40
  /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
41
  declare function slugifyId(input: string): string;
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
35
52
  * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
53
  * Segments are validated and encoded in path order.
37
54
  */
38
- declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
55
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
39
56
 
40
57
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
41
58
  type TelemetryUser = {
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
109
126
  lessonId?: LessonId;
110
127
  data?: InteractionData;
111
128
  });
129
+ /** Payload shape for a telemetry event name. */
130
+ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
131
+ name: N;
132
+ }> extends {
133
+ data?: infer D;
134
+ } ? D : never;
112
135
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
113
136
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
114
137
  type TrackingClient = {
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
143
166
 
144
167
  declare function nowIso(): string;
145
168
 
146
- type BuildTelemetryEventInput = {
147
- name: TelemetryEventName;
169
+ type BuildTelemetryEventContext = {
148
170
  courseId: CourseId;
149
- lessonId?: LessonId;
150
171
  sessionId?: string;
151
172
  attemptId?: string;
152
173
  user?: TelemetryUser;
153
- data?: unknown;
154
174
  timestamp?: string;
155
175
  };
176
+ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
177
+ name: "course_started";
178
+ lessonId?: LessonId;
179
+ data?: undefined;
180
+ }) | (BuildTelemetryEventContext & {
181
+ name: "course_completed";
182
+ lessonId?: LessonId;
183
+ data?: undefined;
184
+ }) | (BuildTelemetryEventContext & {
185
+ name: "lesson_started";
186
+ lessonId?: LessonId;
187
+ data?: LessonLifecycleData;
188
+ }) | (BuildTelemetryEventContext & {
189
+ name: "lesson_completed";
190
+ lessonId?: LessonId;
191
+ data?: LessonLifecycleData;
192
+ }) | (BuildTelemetryEventContext & {
193
+ name: "lesson_time_on_task";
194
+ lessonId?: LessonId;
195
+ data?: LessonLifecycleData;
196
+ }) | (BuildTelemetryEventContext & {
197
+ name: "quiz_answered";
198
+ lessonId?: LessonId;
199
+ data: QuizAnsweredData;
200
+ }) | (BuildTelemetryEventContext & {
201
+ name: "quiz_completed";
202
+ lessonId?: LessonId;
203
+ data: QuizCompletedData;
204
+ }) | (BuildTelemetryEventContext & {
205
+ name: "interaction";
206
+ lessonId?: LessonId;
207
+ data?: InteractionData;
208
+ });
156
209
  /** Reset dev-warning state (tests only). */
157
210
  declare function resetTelemetryBuilderWarningsForTests(): void;
158
211
  /**
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
166
219
  declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
167
220
 
168
221
  type EmitContext = {
169
- courseId: string;
222
+ courseId: CourseId;
170
223
  sessionId?: string;
171
224
  attemptId?: string;
172
225
  };
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
230
283
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
231
284
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
232
285
  declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
286
+ declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
287
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
233
288
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
234
289
 
235
290
  /** Plugin category — aligns with roadmap extension areas. */
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
241
296
  user?: TelemetryUser;
242
297
  };
243
298
  type AssessmentScoreInput = {
244
- checkId: string;
245
- lessonId?: string;
299
+ checkId: CheckId;
300
+ lessonId?: LessonId;
246
301
  response: unknown;
247
302
  };
248
303
  type AssessmentScoreResult = {
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
347
402
  storage?: StoragePort;
348
403
  clock?: ClockPort;
349
404
  };
405
+ type TelemetryEmitFn = {
406
+ <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
407
+ };
350
408
  type HeadlessLessonkitRuntime = {
351
409
  readonly config: HeadlessLessonkitConfig;
352
410
  readonly progress: ProgressController;
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
357
415
  user?: TelemetryUser;
358
416
  };
359
417
  updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
360
- setActiveLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
361
- completeLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
362
- completeCourse: (emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
363
- track: (name: TelemetryEventName, data: unknown | undefined, emit: (event: ReturnType<typeof tryBuildTelemetryEvent>) => void, lessonId?: LessonId) => void;
418
+ setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
419
+ completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
420
+ completeCourse: (emit: TelemetryEmitFn) => void;
421
+ track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
364
422
  resetForCourseChange: (courseId: CourseId) => void;
365
423
  };
366
424
  declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
371
429
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
372
430
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
373
431
 
374
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
432
+ export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ type CourseId = string;
2
2
  type LessonId = string;
3
3
  type CheckId = string;
4
4
  type BlockId = string;
5
+ /** Stable URN string returned by {@link buildLessonkitUrn}. */
6
+ type LessonkitUrn = string;
5
7
  type IdentityValidationIssue = {
6
8
  path: string;
7
9
  message: string;
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
13
15
  ok: false;
14
16
  issues: IdentityValidationIssue[];
15
17
  };
18
+ type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
16
19
  /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
20
  declare const ID_PATTERN: RegExp;
18
21
  declare const ID_MAX_LENGTH = 64;
19
22
 
20
- declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
- declare function assertValidId(input: unknown, path?: string): string;
23
+ /**
24
+ * Exhaustiveness helper for switch/default branches.
25
+ * @throws when called at runtime with an unexpected value.
26
+ */
27
+ declare function assertNever(value: never, message?: string): never;
28
+
29
+ declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
30
+ declare function parseCourseId(input: unknown): CourseId | null;
31
+ declare function parseLessonId(input: unknown): LessonId | null;
32
+ declare function parseCheckId(input: unknown): CheckId | null;
33
+ declare function parseBlockId(input: unknown): BlockId | null;
34
+ declare function assertValidId(input: unknown, path: "courseId"): CourseId;
35
+ declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
36
+ declare function assertValidId(input: unknown, path: "checkId"): CheckId;
37
+ declare function assertValidId(input: unknown, path: "blockId"): BlockId;
38
+ declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
22
39
 
23
40
  /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
41
  declare function slugifyId(input: string): string;
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
35
52
  * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
53
  * Segments are validated and encoded in path order.
37
54
  */
38
- declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
55
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
39
56
 
40
57
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
41
58
  type TelemetryUser = {
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
109
126
  lessonId?: LessonId;
110
127
  data?: InteractionData;
111
128
  });
129
+ /** Payload shape for a telemetry event name. */
130
+ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
131
+ name: N;
132
+ }> extends {
133
+ data?: infer D;
134
+ } ? D : never;
112
135
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
113
136
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
114
137
  type TrackingClient = {
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
143
166
 
144
167
  declare function nowIso(): string;
145
168
 
146
- type BuildTelemetryEventInput = {
147
- name: TelemetryEventName;
169
+ type BuildTelemetryEventContext = {
148
170
  courseId: CourseId;
149
- lessonId?: LessonId;
150
171
  sessionId?: string;
151
172
  attemptId?: string;
152
173
  user?: TelemetryUser;
153
- data?: unknown;
154
174
  timestamp?: string;
155
175
  };
176
+ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
177
+ name: "course_started";
178
+ lessonId?: LessonId;
179
+ data?: undefined;
180
+ }) | (BuildTelemetryEventContext & {
181
+ name: "course_completed";
182
+ lessonId?: LessonId;
183
+ data?: undefined;
184
+ }) | (BuildTelemetryEventContext & {
185
+ name: "lesson_started";
186
+ lessonId?: LessonId;
187
+ data?: LessonLifecycleData;
188
+ }) | (BuildTelemetryEventContext & {
189
+ name: "lesson_completed";
190
+ lessonId?: LessonId;
191
+ data?: LessonLifecycleData;
192
+ }) | (BuildTelemetryEventContext & {
193
+ name: "lesson_time_on_task";
194
+ lessonId?: LessonId;
195
+ data?: LessonLifecycleData;
196
+ }) | (BuildTelemetryEventContext & {
197
+ name: "quiz_answered";
198
+ lessonId?: LessonId;
199
+ data: QuizAnsweredData;
200
+ }) | (BuildTelemetryEventContext & {
201
+ name: "quiz_completed";
202
+ lessonId?: LessonId;
203
+ data: QuizCompletedData;
204
+ }) | (BuildTelemetryEventContext & {
205
+ name: "interaction";
206
+ lessonId?: LessonId;
207
+ data?: InteractionData;
208
+ });
156
209
  /** Reset dev-warning state (tests only). */
157
210
  declare function resetTelemetryBuilderWarningsForTests(): void;
158
211
  /**
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
166
219
  declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
167
220
 
168
221
  type EmitContext = {
169
- courseId: string;
222
+ courseId: CourseId;
170
223
  sessionId?: string;
171
224
  attemptId?: string;
172
225
  };
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
230
283
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
231
284
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
232
285
  declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
286
+ declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
287
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
233
288
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
234
289
 
235
290
  /** Plugin category — aligns with roadmap extension areas. */
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
241
296
  user?: TelemetryUser;
242
297
  };
243
298
  type AssessmentScoreInput = {
244
- checkId: string;
245
- lessonId?: string;
299
+ checkId: CheckId;
300
+ lessonId?: LessonId;
246
301
  response: unknown;
247
302
  };
248
303
  type AssessmentScoreResult = {
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
347
402
  storage?: StoragePort;
348
403
  clock?: ClockPort;
349
404
  };
405
+ type TelemetryEmitFn = {
406
+ <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
407
+ };
350
408
  type HeadlessLessonkitRuntime = {
351
409
  readonly config: HeadlessLessonkitConfig;
352
410
  readonly progress: ProgressController;
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
357
415
  user?: TelemetryUser;
358
416
  };
359
417
  updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
360
- setActiveLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
361
- completeLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
362
- completeCourse: (emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
363
- track: (name: TelemetryEventName, data: unknown | undefined, emit: (event: ReturnType<typeof tryBuildTelemetryEvent>) => void, lessonId?: LessonId) => void;
418
+ setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
419
+ completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
420
+ completeCourse: (emit: TelemetryEmitFn) => void;
421
+ track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
364
422
  resetForCourseChange: (courseId: CourseId) => void;
365
423
  };
366
424
  declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
371
429
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
372
430
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
373
431
 
374
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
432
+ export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
3
3
  var ID_MAX_LENGTH = 64;
4
4
 
5
+ // src/assertNever.ts
6
+ function assertNever(value, message = "Unexpected value") {
7
+ throw new Error(`${message}: ${String(value)}`);
8
+ }
9
+
5
10
  // src/validateId.ts
6
11
  function validateId(input, path = "id") {
7
12
  if (typeof input !== "string") {
@@ -30,6 +35,22 @@ function validateId(input, path = "id") {
30
35
  }
31
36
  return { ok: true, id };
32
37
  }
38
+ function parseCourseId(input) {
39
+ const result = validateId(input, "courseId");
40
+ return result.ok ? result.id : null;
41
+ }
42
+ function parseLessonId(input) {
43
+ const result = validateId(input, "lessonId");
44
+ return result.ok ? result.id : null;
45
+ }
46
+ function parseCheckId(input) {
47
+ const result = validateId(input, "checkId");
48
+ return result.ok ? result.id : null;
49
+ }
50
+ function parseBlockId(input) {
51
+ const result = validateId(input, "blockId");
52
+ return result.ok ? result.id : null;
53
+ }
33
54
  function assertValidId(input, path = "id") {
34
55
  const result = validateId(input, path);
35
56
  if (!result.ok) {
@@ -303,6 +324,11 @@ function isDevEnvironment2() {
303
324
  function resetTelemetryBuilderWarningsForTests() {
304
325
  warnedMissingQuizLesson = false;
305
326
  }
327
+ function resolveLessonId(opts, eventName) {
328
+ const lessonId = opts.lessonId ?? opts.data?.lessonId;
329
+ if (!lessonId) throw new Error(`${eventName} requires lessonId`);
330
+ return lessonId;
331
+ }
306
332
  function buildTelemetryEvent(opts) {
307
333
  const base = {
308
334
  timestamp: opts.timestamp ?? nowIso(),
@@ -317,39 +343,33 @@ function buildTelemetryEvent(opts) {
317
343
  case "course_completed":
318
344
  return { name: "course_completed", ...base };
319
345
  case "lesson_started": {
320
- const data = opts.data;
321
- const lessonId = opts.lessonId ?? data?.lessonId;
322
- if (!lessonId) throw new Error("lesson_started requires lessonId");
346
+ const lessonId = resolveLessonId(opts, "lesson_started");
323
347
  return {
324
348
  name: "lesson_started",
325
349
  ...base,
326
350
  lessonId,
327
- data: { ...data, lessonId }
351
+ data: { ...opts.data, lessonId }
328
352
  };
329
353
  }
330
354
  case "lesson_completed":
331
355
  case "lesson_time_on_task": {
332
- const data = opts.data;
333
- const lessonId = opts.lessonId ?? data?.lessonId;
334
- if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
356
+ const lessonId = resolveLessonId(opts, opts.name);
335
357
  return {
336
358
  name: opts.name,
337
359
  ...base,
338
360
  lessonId,
339
- data: { ...data, lessonId }
361
+ data: { ...opts.data, lessonId }
340
362
  };
341
363
  }
342
364
  case "quiz_answered": {
343
- const data = opts.data;
344
365
  const lessonId = opts.lessonId;
345
366
  if (!lessonId) throw new Error("quiz_answered requires active lessonId");
346
- return { name: "quiz_answered", ...base, lessonId, data };
367
+ return { name: "quiz_answered", ...base, lessonId, data: opts.data };
347
368
  }
348
369
  case "quiz_completed": {
349
- const data = opts.data;
350
370
  const lessonId = opts.lessonId;
351
371
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
352
- return { name: "quiz_completed", ...base, lessonId, data };
372
+ return { name: "quiz_completed", ...base, lessonId, data: opts.data };
353
373
  }
354
374
  case "interaction":
355
375
  return {
@@ -359,7 +379,7 @@ function buildTelemetryEvent(opts) {
359
379
  data: opts.data
360
380
  };
361
381
  default:
362
- return { name: opts.name, ...base };
382
+ return assertNever(opts);
363
383
  }
364
384
  }
365
385
  function tryBuildTelemetryEvent(opts) {
@@ -556,6 +576,7 @@ function getTabSessionId(storage) {
556
576
  }
557
577
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
558
578
  var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
579
+ var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
559
580
  function resolveSessionId(storage, provided) {
560
581
  if (provided) return provided;
561
582
  const existing = storage.getItem(SESSION_STORAGE_KEY);
@@ -570,6 +591,9 @@ function courseStartedStorageKey(sessionId, courseId) {
570
591
  function courseStartedTrackingStorageKey(sessionId, courseId) {
571
592
  return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
572
593
  }
594
+ function courseStartedPipelineStorageKey(sessionId, courseId) {
595
+ return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
596
+ }
573
597
  function hasCourseStarted(storage, sessionId, courseId) {
574
598
  if (!courseId) return false;
575
599
  return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
@@ -586,6 +610,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
586
610
  if (!courseId) return;
587
611
  storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
588
612
  }
613
+ function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
614
+ if (!courseId) return false;
615
+ return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
616
+ }
617
+ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
618
+ if (!courseId) return;
619
+ storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
620
+ }
589
621
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
590
622
  if (!courseId || fromSessionId === toSessionId) return;
591
623
  if (hasCourseStarted(storage, fromSessionId, courseId)) {
@@ -596,6 +628,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
596
628
  markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
597
629
  storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
598
630
  }
631
+ if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
632
+ markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
633
+ storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
634
+ }
599
635
  }
600
636
 
601
637
  // src/runtime/courseLifecycle.ts
@@ -844,6 +880,7 @@ export {
844
880
  ID_PATTERN,
845
881
  SESSION_STORAGE_KEY,
846
882
  TELEMETRY_EVENT_CATALOG,
883
+ assertNever,
847
884
  assertValidId,
848
885
  buildCourseStartedTelemetryEvent,
849
886
  buildLessonkitUrn,
@@ -869,10 +906,16 @@ export {
869
906
  getTabSessionId,
870
907
  hasCourseStarted,
871
908
  hasCourseStartedEmittedToTracking,
909
+ hasCourseStartedPipelineDelivered,
872
910
  markCourseStarted,
873
911
  markCourseStartedEmittedToTracking,
912
+ markCourseStartedPipelineDelivered,
874
913
  migrateCourseStartedMark,
875
914
  nowIso,
915
+ parseBlockId,
916
+ parseCheckId,
917
+ parseCourseId,
918
+ parseLessonId,
876
919
  resetStoragePortForTests,
877
920
  resetTelemetryBuilderWarningsForTests,
878
921
  resolveSessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",