@lessonkit/core 0.9.3 → 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.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,138 @@ declare function createSessionId(): string;
143
166
 
144
167
  declare function nowIso(): string;
145
168
 
169
+ type BuildTelemetryEventContext = {
170
+ courseId: CourseId;
171
+ sessionId?: string;
172
+ attemptId?: string;
173
+ user?: TelemetryUser;
174
+ timestamp?: string;
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
+ });
209
+ /** Reset dev-warning state (tests only). */
210
+ declare function resetTelemetryBuilderWarningsForTests(): void;
211
+ /**
212
+ * Build a typed telemetry event from a catalog event name and context.
213
+ * Validates lesson-scoped events require `lessonId`.
214
+ */
215
+ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent;
216
+ /**
217
+ * Like `buildTelemetryEvent`, but returns null (with a dev warning) when quiz events lack an active lesson.
218
+ */
219
+ declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
220
+
221
+ type EmitContext = {
222
+ courseId: CourseId;
223
+ sessionId?: string;
224
+ attemptId?: string;
225
+ };
226
+ /** Pluggable telemetry output (OCP). Distinct from the legacy `TelemetrySink` function type. */
227
+ type TelemetryPipelineSink = {
228
+ readonly id: string;
229
+ emit(event: TelemetryEvent, ctx: EmitContext): void | Promise<void>;
230
+ };
231
+ type TelemetryPipeline = {
232
+ readonly sinks: readonly TelemetryPipelineSink[];
233
+ emit(event: TelemetryEvent, ctx?: EmitContext): void | Promise<void>;
234
+ };
235
+ declare function createTelemetryPipeline(sinks: TelemetryPipelineSink[]): TelemetryPipeline;
236
+ declare function createTrackingPipelineSink(id: string, track: (event: TelemetryEvent) => void): TelemetryPipelineSink;
237
+
238
+ type StoragePort = {
239
+ getItem: (key: string) => string | null;
240
+ setItem: (key: string, value: string) => void;
241
+ removeItem?: (key: string) => void;
242
+ /** @internal Test helper to clear in-memory fallback state. */
243
+ resetForTests?: () => void;
244
+ };
245
+ type ClockPort = {
246
+ nowMs: () => number;
247
+ nowIso: () => string;
248
+ };
249
+ type TimerPort = {
250
+ setInterval: (fn: () => void, ms: number) => ReturnType<typeof globalThis.setInterval>;
251
+ clearInterval: (id: ReturnType<typeof globalThis.setInterval>) => void;
252
+ };
253
+ declare function createDefaultClock(): ClockPort;
254
+ declare function createNoopStorage(): StoragePort;
255
+ declare function resetStoragePortForTests(storage: StoragePort): void;
256
+ declare function createSessionStoragePort(): StoragePort;
257
+ declare function createGlobalTimer(): TimerPort;
258
+
259
+ type ProgressState = {
260
+ activeLessonId?: LessonId;
261
+ completedLessonIds: ReadonlySet<LessonId>;
262
+ courseCompleted: boolean;
263
+ };
264
+ type ProgressController = {
265
+ getState: () => ProgressState;
266
+ setActiveLesson: (lessonId: LessonId, startedAtMs: number) => {
267
+ previousLessonId?: LessonId;
268
+ };
269
+ completeLesson: (lessonId: LessonId, completedAtMs: number) => {
270
+ durationMs?: number;
271
+ didComplete: boolean;
272
+ };
273
+ completeCourse: () => {
274
+ didComplete: boolean;
275
+ };
276
+ };
277
+ declare function createProgressController(): ProgressController;
278
+
279
+ declare const SESSION_STORAGE_KEY = "lessonkit:sessionId";
280
+ declare function getTabSessionId(storage: StoragePort): string | null;
281
+ declare function resolveSessionId(storage: StoragePort, provided?: string): string;
282
+ declare function hasCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
283
+ declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
284
+ declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
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;
288
+ declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
289
+
146
290
  /** Plugin category — aligns with roadmap extension areas. */
147
291
  type LessonkitPluginKind = "analytics" | "lms" | "assessment" | "interaction" | "ai";
148
292
  type LessonkitPluginContext = {
149
293
  courseId: CourseId;
150
294
  sessionId?: string;
151
295
  attemptId?: string;
296
+ user?: TelemetryUser;
152
297
  };
153
298
  type AssessmentScoreInput = {
154
- checkId: string;
155
- lessonId?: string;
299
+ checkId: CheckId;
300
+ lessonId?: LessonId;
156
301
  response: unknown;
157
302
  };
158
303
  type AssessmentScoreResult = {
@@ -167,43 +312,121 @@ type InteractionBlockRegistration = {
167
312
  catalogVersion?: string;
168
313
  description?: string;
169
314
  };
170
- /**
171
- * Framework plugin contract (v1). Plugins are plain objects registered on `LessonkitProvider`.
172
- * Marketplace / dynamic loading are out of scope until Studio post-1.0.
173
- */
174
- type LessonkitPlugin = {
315
+ type PluginIdentity = {
175
316
  id: string;
176
317
  version: string;
177
318
  kind: LessonkitPluginKind;
178
319
  name?: string;
179
- setup?: (ctx: LessonkitPluginContext) => void;
180
- dispose?: () => void;
181
- /**
182
- * Observe or filter telemetry before tracking/xAPI. Return `null` to drop the event.
183
- * Hooks run in registration order.
184
- */
320
+ };
321
+ /** Narrow telemetry plugin contract (ISP). */
322
+ type TelemetryPlugin = PluginIdentity & {
185
323
  onTelemetry?: (event: TelemetryEvent, ctx: LessonkitPluginContext) => TelemetryEvent | null;
186
- /** Optional batch observer (analytics); receives events after per-event hooks. */
187
324
  onTelemetryBatch?: (events: TelemetryEvent[], ctx: LessonkitPluginContext) => void;
188
- /** Wrap the configured tracking sink (analytics plugins). First registered = innermost. */
189
325
  wrapTrackingSink?: (sink: TelemetrySink, ctx: LessonkitPluginContext) => TelemetrySink;
190
- /** Optional assessment scoring override (assessment plugins). */
326
+ };
327
+ /** Narrow lifecycle plugin contract (ISP). */
328
+ type LifecyclePlugin = PluginIdentity & {
329
+ setup?: (ctx: LessonkitPluginContext) => void;
330
+ dispose?: () => void;
331
+ };
332
+ /** Narrow assessment plugin contract (ISP). */
333
+ type AssessmentPlugin = PluginIdentity & {
334
+ kind: "assessment";
191
335
  scoreAssessment?: (input: AssessmentScoreInput, ctx: LessonkitPluginContext) => AssessmentScoreResult | null;
192
- /** Declare custom interaction block types for generators/tooling. */
336
+ };
337
+ /** Narrow interaction metadata plugin (ISP). */
338
+ type InteractionPlugin = PluginIdentity & {
193
339
  interactionBlocks?: InteractionBlockRegistration[];
194
340
  };
341
+ /**
342
+ * Combined plugin contract (v1). Prefer segregated types for new plugins.
343
+ * @deprecated Prefer `TelemetryPlugin`, `AssessmentPlugin`, or `LifecyclePlugin` via `define*Plugin`.
344
+ */
345
+ type LessonkitPlugin = PluginIdentity & Partial<Pick<TelemetryPlugin, "onTelemetry" | "onTelemetryBatch" | "wrapTrackingSink"> & Pick<LifecyclePlugin, "setup" | "dispose"> & Pick<AssessmentPlugin, "scoreAssessment"> & Pick<InteractionPlugin, "interactionBlocks">>;
195
346
  type PluginHost = {
196
347
  readonly plugins: readonly LessonkitPlugin[];
197
348
  setupAll: (ctx: LessonkitPluginContext) => void;
198
349
  disposeAll: () => void;
199
350
  runTelemetry: (event: TelemetryEvent, ctx: LessonkitPluginContext) => TelemetryEvent | null;
200
351
  runTelemetryBatch: (events: TelemetryEvent[], ctx: LessonkitPluginContext) => TelemetryEvent[];
201
- /** Invoke only `onTelemetryBatch` hooks; events were already filtered at emit time. */
202
352
  deliverTelemetryBatch: (events: TelemetryEvent[], ctx: LessonkitPluginContext) => TelemetryEvent[];
203
353
  composeTrackingSink: (sink: TelemetrySink | undefined, ctx: LessonkitPluginContext | (() => LessonkitPluginContext)) => TelemetrySink | undefined;
204
354
  scoreAssessment: (input: AssessmentScoreInput, ctx: LessonkitPluginContext) => AssessmentScoreResult | null;
205
355
  };
206
- declare function defineLessonkitPlugin(plugin: LessonkitPlugin): LessonkitPlugin;
207
- declare function createPluginHost(plugins?: readonly LessonkitPlugin[]): PluginHost;
356
+ /** Segregated plugin registry (ISP + SRP). */
357
+ type PluginRegistry = PluginHost;
358
+
359
+ type CourseLifecycleContext = {
360
+ courseId: CourseId;
361
+ sessionId: string;
362
+ attemptId?: string;
363
+ user?: TelemetryUser;
364
+ storage: StoragePort;
365
+ pluginHost: PluginRegistry | null;
366
+ lxpackBridge: "auto" | "off";
367
+ };
368
+ type CourseLifecycleDeps = {
369
+ emitCourseStartedEvent: (ctx: CourseLifecycleContext) => boolean;
370
+ };
371
+ declare function tryEmitCourseStarted(ctx: CourseLifecycleContext, deps: CourseLifecycleDeps, alreadyEmittedToSink: boolean): {
372
+ emitted: boolean;
373
+ marked: boolean;
374
+ };
375
+ declare function buildCourseStartedTelemetryEvent(ctx: CourseLifecycleContext): TelemetryEvent;
376
+ type LessonCompletionEmitter = (lessonId: LessonId, durationMs?: number) => void;
377
+ declare function completeLessonWithTelemetry(opts: {
378
+ progress: ProgressController;
379
+ lessonId: LessonId;
380
+ nowMs: number;
381
+ emitLessonCompleted: LessonCompletionEmitter;
382
+ }): boolean;
383
+ declare function completeCourseWithTelemetry(opts: {
384
+ progress: ProgressController;
385
+ nowMs: number;
386
+ emitLessonCompleted: LessonCompletionEmitter;
387
+ emitCourseCompleted: () => void;
388
+ }): boolean;
389
+
390
+ type LessonkitRuntimeVersion = "v1" | "v2";
391
+ type HeadlessLessonkitConfig = {
392
+ courseId: CourseId;
393
+ runtimeVersion?: LessonkitRuntimeVersion;
394
+ session?: {
395
+ sessionId?: string;
396
+ attemptId?: string;
397
+ user?: TelemetryUser;
398
+ };
399
+ plugins?: PluginRegistry | null;
400
+ };
401
+ type HeadlessRuntimePorts = {
402
+ storage?: StoragePort;
403
+ clock?: ClockPort;
404
+ };
405
+ type TelemetryEmitFn = {
406
+ <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
407
+ };
408
+ type HeadlessLessonkitRuntime = {
409
+ readonly config: HeadlessLessonkitConfig;
410
+ readonly progress: ProgressController;
411
+ getProgressState: () => ProgressState;
412
+ getSession: () => {
413
+ sessionId: string;
414
+ attemptId?: string;
415
+ user?: TelemetryUser;
416
+ };
417
+ updateConfig: (next: Partial<HeadlessLessonkitConfig>) => 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;
422
+ resetForCourseChange: (courseId: CourseId) => void;
423
+ };
424
+ declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
425
+
426
+ declare function createPluginRegistry(plugins?: readonly LessonkitPlugin[]): PluginRegistry;
427
+
428
+ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin;
429
+ declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
430
+ declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
208
431
 
209
- export { type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type CheckId, type CourseId, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitUrnParts, type PluginHost, type QuizAnsweredData, type QuizCompletedData, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetrySink, type TelemetryUser, type TrackingClient, assertValidId, buildLessonkitUrn, buildTelemetryCatalog, createPluginHost, createSessionId, createTrackingClient, defineLessonkitPlugin, deriveId, nowIso, slugifyId, telemetryCatalogVersion, 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 };