@lessonkit/core 0.3.1 → 0.5.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/README.md CHANGED
@@ -12,9 +12,12 @@ Core types and runtime primitives shared across LessonKit packages.
12
12
  npm install @lessonkit/core
13
13
  ```
14
14
 
15
- ## What’s inside (0.3.0)
15
+ ## What’s inside (0.5.0)
16
16
 
17
- - Telemetry event types (`TelemetryEvent`)
18
- - A minimal tracking client (`createTrackingClient`) with optional batching
17
+ - Identity helpers: `validateId`, `slugifyId`, `deriveId`, `buildLessonkitUrn`
18
+ - Typed telemetry events (`TelemetryEvent`) and `telemetry-catalog.v1.json`
19
+ - Tracking client (`createTrackingClient`) with optional batching
19
20
  - Session id helper (`createSessionId`)
20
21
 
22
+ See [`docs/IDENTITY.md`](../../docs/IDENTITY.md) and [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md).
23
+
package/dist/index.cjs CHANGED
@@ -20,12 +20,177 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ID_MAX_LENGTH: () => ID_MAX_LENGTH,
24
+ ID_PATTERN: () => ID_PATTERN,
25
+ TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
26
+ assertValidId: () => assertValidId,
27
+ buildLessonkitUrn: () => buildLessonkitUrn,
28
+ buildTelemetryCatalog: () => buildTelemetryCatalog,
23
29
  createSessionId: () => createSessionId,
24
30
  createTrackingClient: () => createTrackingClient,
25
- nowIso: () => nowIso
31
+ deriveId: () => deriveId,
32
+ nowIso: () => nowIso,
33
+ slugifyId: () => slugifyId,
34
+ telemetryCatalogVersion: () => telemetryCatalogVersion,
35
+ validateId: () => validateId
26
36
  });
27
37
  module.exports = __toCommonJS(index_exports);
28
38
 
39
+ // src/identityTypes.ts
40
+ var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
41
+ var ID_MAX_LENGTH = 64;
42
+
43
+ // src/validateId.ts
44
+ function validateId(input, path = "id") {
45
+ if (typeof input !== "string") {
46
+ return { ok: false, issues: [{ path, message: "id must be a string" }] };
47
+ }
48
+ const id = input.trim();
49
+ if (!id.length) {
50
+ return { ok: false, issues: [{ path, message: "id must not be empty" }] };
51
+ }
52
+ if (id.length > ID_MAX_LENGTH) {
53
+ return {
54
+ ok: false,
55
+ issues: [{ path, message: `id must be at most ${ID_MAX_LENGTH} characters` }]
56
+ };
57
+ }
58
+ if (!ID_PATTERN.test(id)) {
59
+ return {
60
+ ok: false,
61
+ issues: [
62
+ {
63
+ path,
64
+ message: "id must start with a letter and contain only letters, digits, underscores, and hyphens"
65
+ }
66
+ ]
67
+ };
68
+ }
69
+ return { ok: true, id };
70
+ }
71
+ function assertValidId(input, path = "id") {
72
+ const result = validateId(input, path);
73
+ if (!result.ok) {
74
+ throw new Error(result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
75
+ }
76
+ return result.id;
77
+ }
78
+
79
+ // src/slugify.ts
80
+ function slugifyId(input) {
81
+ const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
82
+ if (!slug.length) return "id";
83
+ const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`;
84
+ const validated = validateId(candidate);
85
+ return validated.ok ? validated.id : "id";
86
+ }
87
+ function deriveId(title, usedIds = /* @__PURE__ */ new Set()) {
88
+ const base = slugifyId(title);
89
+ if (!usedIds.has(base)) return base;
90
+ for (let n = 2; n < 1e3; n++) {
91
+ const candidate = `${base}-${n}`;
92
+ if (!usedIds.has(candidate)) return candidate;
93
+ }
94
+ return `${base}-${Date.now()}`;
95
+ }
96
+
97
+ // src/urn.ts
98
+ function buildLessonkitUrn(parts) {
99
+ const courseId = assertValidId(parts.courseId, "courseId");
100
+ let urn = `urn:lessonkit:course:${courseId}`;
101
+ if (parts.lessonId !== void 0) {
102
+ const lessonId = assertValidId(parts.lessonId, "lessonId");
103
+ urn += `:lesson:${lessonId}`;
104
+ }
105
+ if (parts.checkId !== void 0) {
106
+ const checkId = assertValidId(parts.checkId, "checkId");
107
+ if (parts.lessonId === void 0) {
108
+ throw new Error("buildLessonkitUrn: checkId requires lessonId");
109
+ }
110
+ urn += `:check:${checkId}`;
111
+ }
112
+ if (parts.blockId !== void 0) {
113
+ const blockId = assertValidId(parts.blockId, "blockId");
114
+ if (parts.lessonId === void 0) {
115
+ throw new Error("buildLessonkitUrn: blockId requires lessonId");
116
+ }
117
+ urn += `:block:${blockId}`;
118
+ }
119
+ return urn;
120
+ }
121
+
122
+ // src/telemetryCatalog.ts
123
+ var telemetryCatalogVersion = 1;
124
+ var TELEMETRY_EVENT_CATALOG = [
125
+ {
126
+ name: "course_started",
127
+ description: "Learner session began for a course",
128
+ requiredFields: ["courseId", "sessionId", "timestamp"],
129
+ dataFields: [],
130
+ xapiVerb: "http://adlnet.gov/expapi/verbs/initialized",
131
+ urnPattern: "urn:lessonkit:course:{courseId}"
132
+ },
133
+ {
134
+ name: "course_completed",
135
+ description: "Course completion criteria met",
136
+ requiredFields: ["courseId", "sessionId", "timestamp"],
137
+ dataFields: [],
138
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
139
+ urnPattern: "urn:lessonkit:course:{courseId}"
140
+ },
141
+ {
142
+ name: "lesson_started",
143
+ description: "Lesson became active",
144
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
145
+ dataFields: ["lessonId"],
146
+ xapiVerb: "http://adlnet.gov/expapi/verbs/initialized",
147
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
148
+ },
149
+ {
150
+ name: "lesson_completed",
151
+ description: "Lesson marked complete",
152
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
153
+ dataFields: ["lessonId", "durationMs"],
154
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
155
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
156
+ },
157
+ {
158
+ name: "lesson_time_on_task",
159
+ description: "Time on task for a lesson (companion to lesson_completed)",
160
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
161
+ dataFields: ["lessonId", "durationMs"],
162
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
163
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
164
+ },
165
+ {
166
+ name: "quiz_answered",
167
+ description: "Learner submitted a quiz/check answer",
168
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
169
+ dataFields: ["checkId", "question", "choice", "correct"],
170
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
171
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
172
+ },
173
+ {
174
+ name: "quiz_completed",
175
+ description: "Quiz/check completed (e.g. first correct answer)",
176
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
177
+ dataFields: ["checkId", "score", "maxScore"],
178
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
179
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
180
+ },
181
+ {
182
+ name: "interaction",
183
+ description: "Custom interaction (branching, UI actions, blocks)",
184
+ requiredFields: ["courseId", "sessionId", "timestamp"],
185
+ dataFields: ["kind", "blockId", "payload"],
186
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
187
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
188
+ }
189
+ ];
190
+ function buildTelemetryCatalog() {
191
+ return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
192
+ }
193
+
29
194
  // src/trackingClient.ts
30
195
  function createTrackingClient(opts) {
31
196
  const sink = opts?.sink;
@@ -110,7 +275,17 @@ function nowIso() {
110
275
  }
111
276
  // Annotate the CommonJS export names for ESM import in node:
112
277
  0 && (module.exports = {
278
+ ID_MAX_LENGTH,
279
+ ID_PATTERN,
280
+ TELEMETRY_EVENT_CATALOG,
281
+ assertValidId,
282
+ buildLessonkitUrn,
283
+ buildTelemetryCatalog,
113
284
  createSessionId,
114
285
  createTrackingClient,
115
- nowIso
286
+ deriveId,
287
+ nowIso,
288
+ slugifyId,
289
+ telemetryCatalogVersion,
290
+ validateId
116
291
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,42 @@
1
1
  type CourseId = string;
2
2
  type LessonId = string;
3
+ type CheckId = string;
4
+ type BlockId = string;
5
+ type IdentityValidationIssue = {
6
+ path: string;
7
+ message: string;
8
+ };
9
+ type IdentityValidationResult = {
10
+ ok: true;
11
+ id: string;
12
+ } | {
13
+ ok: false;
14
+ issues: IdentityValidationIssue[];
15
+ };
16
+ /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
+ declare const ID_PATTERN: RegExp;
18
+ declare const ID_MAX_LENGTH = 64;
19
+
20
+ declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
+ declare function assertValidId(input: unknown, path?: string): string;
22
+
23
+ /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
+ declare function slugifyId(input: string): string;
25
+ /** Pick a unique id from a title, suffixing -2, -3, … on collision. */
26
+ declare function deriveId(title: string, usedIds?: ReadonlySet<string>): string;
27
+
28
+ type LessonkitUrnParts = {
29
+ courseId: CourseId;
30
+ lessonId?: LessonId;
31
+ checkId?: CheckId;
32
+ blockId?: BlockId;
33
+ };
34
+ /**
35
+ * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
+ * Segments are validated and encoded in path order.
37
+ */
38
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
39
+
3
40
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
4
41
  type TelemetryUser = {
5
42
  id?: string;
@@ -7,16 +44,70 @@ type TelemetryUser = {
7
44
  name?: string;
8
45
  [key: string]: unknown;
9
46
  };
10
- type TelemetryEvent = {
11
- name: TelemetryEventName;
47
+ type TelemetryEventBase = {
12
48
  timestamp: string;
13
- courseId?: CourseId;
14
- lessonId?: LessonId;
49
+ courseId: CourseId;
15
50
  sessionId?: string;
16
51
  attemptId?: string;
17
52
  user?: TelemetryUser;
18
- data?: Record<string, unknown>;
19
53
  };
54
+ type LessonLifecycleData = {
55
+ lessonId: LessonId;
56
+ durationMs?: number;
57
+ success?: boolean;
58
+ score?: number;
59
+ maxScore?: number;
60
+ };
61
+ type QuizAnsweredData = {
62
+ checkId: CheckId;
63
+ question: string;
64
+ choice: string;
65
+ correct: boolean;
66
+ };
67
+ type QuizCompletedData = {
68
+ checkId: CheckId;
69
+ score?: number;
70
+ maxScore?: number;
71
+ };
72
+ type InteractionData = {
73
+ kind?: string;
74
+ blockId?: BlockId;
75
+ payload?: Record<string, unknown>;
76
+ [key: string]: unknown;
77
+ };
78
+ type TelemetryEvent = (TelemetryEventBase & {
79
+ name: "course_started";
80
+ lessonId?: LessonId;
81
+ data?: undefined;
82
+ }) | (TelemetryEventBase & {
83
+ name: "course_completed";
84
+ lessonId?: LessonId;
85
+ data?: undefined;
86
+ }) | (TelemetryEventBase & {
87
+ name: "lesson_started";
88
+ lessonId: LessonId;
89
+ data: LessonLifecycleData;
90
+ }) | (TelemetryEventBase & {
91
+ name: "lesson_completed";
92
+ lessonId: LessonId;
93
+ data: LessonLifecycleData;
94
+ }) | (TelemetryEventBase & {
95
+ name: "lesson_time_on_task";
96
+ lessonId: LessonId;
97
+ data: LessonLifecycleData;
98
+ }) | (TelemetryEventBase & {
99
+ name: "quiz_answered";
100
+ lessonId: LessonId;
101
+ data: QuizAnsweredData;
102
+ }) | (TelemetryEventBase & {
103
+ name: "quiz_completed";
104
+ lessonId: LessonId;
105
+ data: QuizCompletedData;
106
+ }) | (TelemetryEventBase & {
107
+ name: "interaction";
108
+ lessonId?: LessonId;
109
+ data?: InteractionData;
110
+ });
20
111
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
21
112
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
22
113
  type TrackingClient = {
@@ -25,6 +116,18 @@ type TrackingClient = {
25
116
  dispose?: () => void;
26
117
  };
27
118
 
119
+ declare const telemetryCatalogVersion: 1;
120
+ type TelemetryCatalogEntry = {
121
+ name: TelemetryEventName;
122
+ description: string;
123
+ requiredFields: string[];
124
+ dataFields: string[];
125
+ xapiVerb: string;
126
+ urnPattern: string;
127
+ };
128
+ declare const TELEMETRY_EVENT_CATALOG: TelemetryCatalogEntry[];
129
+ declare function buildTelemetryCatalog(): TelemetryCatalogEntry[];
130
+
28
131
  declare function createTrackingClient(opts?: {
29
132
  sink?: TelemetrySink;
30
133
  batch?: {
@@ -39,4 +142,4 @@ declare function createSessionId(): string;
39
142
 
40
143
  declare function nowIso(): string;
41
144
 
42
- export { type CourseId, type LessonId, type TelemetryBatchSink, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TelemetryUser, type TrackingClient, createSessionId, createTrackingClient, nowIso };
145
+ export { type BlockId, type CheckId, type CourseId, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionData, type LessonId, type LessonLifecycleData, type LessonkitUrnParts, 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, createSessionId, createTrackingClient, deriveId, nowIso, slugifyId, telemetryCatalogVersion, validateId };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,42 @@
1
1
  type CourseId = string;
2
2
  type LessonId = string;
3
+ type CheckId = string;
4
+ type BlockId = string;
5
+ type IdentityValidationIssue = {
6
+ path: string;
7
+ message: string;
8
+ };
9
+ type IdentityValidationResult = {
10
+ ok: true;
11
+ id: string;
12
+ } | {
13
+ ok: false;
14
+ issues: IdentityValidationIssue[];
15
+ };
16
+ /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
+ declare const ID_PATTERN: RegExp;
18
+ declare const ID_MAX_LENGTH = 64;
19
+
20
+ declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
+ declare function assertValidId(input: unknown, path?: string): string;
22
+
23
+ /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
+ declare function slugifyId(input: string): string;
25
+ /** Pick a unique id from a title, suffixing -2, -3, … on collision. */
26
+ declare function deriveId(title: string, usedIds?: ReadonlySet<string>): string;
27
+
28
+ type LessonkitUrnParts = {
29
+ courseId: CourseId;
30
+ lessonId?: LessonId;
31
+ checkId?: CheckId;
32
+ blockId?: BlockId;
33
+ };
34
+ /**
35
+ * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
+ * Segments are validated and encoded in path order.
37
+ */
38
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
39
+
3
40
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
4
41
  type TelemetryUser = {
5
42
  id?: string;
@@ -7,16 +44,70 @@ type TelemetryUser = {
7
44
  name?: string;
8
45
  [key: string]: unknown;
9
46
  };
10
- type TelemetryEvent = {
11
- name: TelemetryEventName;
47
+ type TelemetryEventBase = {
12
48
  timestamp: string;
13
- courseId?: CourseId;
14
- lessonId?: LessonId;
49
+ courseId: CourseId;
15
50
  sessionId?: string;
16
51
  attemptId?: string;
17
52
  user?: TelemetryUser;
18
- data?: Record<string, unknown>;
19
53
  };
54
+ type LessonLifecycleData = {
55
+ lessonId: LessonId;
56
+ durationMs?: number;
57
+ success?: boolean;
58
+ score?: number;
59
+ maxScore?: number;
60
+ };
61
+ type QuizAnsweredData = {
62
+ checkId: CheckId;
63
+ question: string;
64
+ choice: string;
65
+ correct: boolean;
66
+ };
67
+ type QuizCompletedData = {
68
+ checkId: CheckId;
69
+ score?: number;
70
+ maxScore?: number;
71
+ };
72
+ type InteractionData = {
73
+ kind?: string;
74
+ blockId?: BlockId;
75
+ payload?: Record<string, unknown>;
76
+ [key: string]: unknown;
77
+ };
78
+ type TelemetryEvent = (TelemetryEventBase & {
79
+ name: "course_started";
80
+ lessonId?: LessonId;
81
+ data?: undefined;
82
+ }) | (TelemetryEventBase & {
83
+ name: "course_completed";
84
+ lessonId?: LessonId;
85
+ data?: undefined;
86
+ }) | (TelemetryEventBase & {
87
+ name: "lesson_started";
88
+ lessonId: LessonId;
89
+ data: LessonLifecycleData;
90
+ }) | (TelemetryEventBase & {
91
+ name: "lesson_completed";
92
+ lessonId: LessonId;
93
+ data: LessonLifecycleData;
94
+ }) | (TelemetryEventBase & {
95
+ name: "lesson_time_on_task";
96
+ lessonId: LessonId;
97
+ data: LessonLifecycleData;
98
+ }) | (TelemetryEventBase & {
99
+ name: "quiz_answered";
100
+ lessonId: LessonId;
101
+ data: QuizAnsweredData;
102
+ }) | (TelemetryEventBase & {
103
+ name: "quiz_completed";
104
+ lessonId: LessonId;
105
+ data: QuizCompletedData;
106
+ }) | (TelemetryEventBase & {
107
+ name: "interaction";
108
+ lessonId?: LessonId;
109
+ data?: InteractionData;
110
+ });
20
111
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
21
112
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
22
113
  type TrackingClient = {
@@ -25,6 +116,18 @@ type TrackingClient = {
25
116
  dispose?: () => void;
26
117
  };
27
118
 
119
+ declare const telemetryCatalogVersion: 1;
120
+ type TelemetryCatalogEntry = {
121
+ name: TelemetryEventName;
122
+ description: string;
123
+ requiredFields: string[];
124
+ dataFields: string[];
125
+ xapiVerb: string;
126
+ urnPattern: string;
127
+ };
128
+ declare const TELEMETRY_EVENT_CATALOG: TelemetryCatalogEntry[];
129
+ declare function buildTelemetryCatalog(): TelemetryCatalogEntry[];
130
+
28
131
  declare function createTrackingClient(opts?: {
29
132
  sink?: TelemetrySink;
30
133
  batch?: {
@@ -39,4 +142,4 @@ declare function createSessionId(): string;
39
142
 
40
143
  declare function nowIso(): string;
41
144
 
42
- export { type CourseId, type LessonId, type TelemetryBatchSink, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TelemetryUser, type TrackingClient, createSessionId, createTrackingClient, nowIso };
145
+ export { type BlockId, type CheckId, type CourseId, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionData, type LessonId, type LessonLifecycleData, type LessonkitUrnParts, 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, createSessionId, createTrackingClient, deriveId, nowIso, slugifyId, telemetryCatalogVersion, validateId };
package/dist/index.js CHANGED
@@ -1,3 +1,158 @@
1
+ // src/identityTypes.ts
2
+ var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
3
+ var ID_MAX_LENGTH = 64;
4
+
5
+ // src/validateId.ts
6
+ function validateId(input, path = "id") {
7
+ if (typeof input !== "string") {
8
+ return { ok: false, issues: [{ path, message: "id must be a string" }] };
9
+ }
10
+ const id = input.trim();
11
+ if (!id.length) {
12
+ return { ok: false, issues: [{ path, message: "id must not be empty" }] };
13
+ }
14
+ if (id.length > ID_MAX_LENGTH) {
15
+ return {
16
+ ok: false,
17
+ issues: [{ path, message: `id must be at most ${ID_MAX_LENGTH} characters` }]
18
+ };
19
+ }
20
+ if (!ID_PATTERN.test(id)) {
21
+ return {
22
+ ok: false,
23
+ issues: [
24
+ {
25
+ path,
26
+ message: "id must start with a letter and contain only letters, digits, underscores, and hyphens"
27
+ }
28
+ ]
29
+ };
30
+ }
31
+ return { ok: true, id };
32
+ }
33
+ function assertValidId(input, path = "id") {
34
+ const result = validateId(input, path);
35
+ if (!result.ok) {
36
+ throw new Error(result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
37
+ }
38
+ return result.id;
39
+ }
40
+
41
+ // src/slugify.ts
42
+ function slugifyId(input) {
43
+ const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
44
+ if (!slug.length) return "id";
45
+ const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`;
46
+ const validated = validateId(candidate);
47
+ return validated.ok ? validated.id : "id";
48
+ }
49
+ function deriveId(title, usedIds = /* @__PURE__ */ new Set()) {
50
+ const base = slugifyId(title);
51
+ if (!usedIds.has(base)) return base;
52
+ for (let n = 2; n < 1e3; n++) {
53
+ const candidate = `${base}-${n}`;
54
+ if (!usedIds.has(candidate)) return candidate;
55
+ }
56
+ return `${base}-${Date.now()}`;
57
+ }
58
+
59
+ // src/urn.ts
60
+ function buildLessonkitUrn(parts) {
61
+ const courseId = assertValidId(parts.courseId, "courseId");
62
+ let urn = `urn:lessonkit:course:${courseId}`;
63
+ if (parts.lessonId !== void 0) {
64
+ const lessonId = assertValidId(parts.lessonId, "lessonId");
65
+ urn += `:lesson:${lessonId}`;
66
+ }
67
+ if (parts.checkId !== void 0) {
68
+ const checkId = assertValidId(parts.checkId, "checkId");
69
+ if (parts.lessonId === void 0) {
70
+ throw new Error("buildLessonkitUrn: checkId requires lessonId");
71
+ }
72
+ urn += `:check:${checkId}`;
73
+ }
74
+ if (parts.blockId !== void 0) {
75
+ const blockId = assertValidId(parts.blockId, "blockId");
76
+ if (parts.lessonId === void 0) {
77
+ throw new Error("buildLessonkitUrn: blockId requires lessonId");
78
+ }
79
+ urn += `:block:${blockId}`;
80
+ }
81
+ return urn;
82
+ }
83
+
84
+ // src/telemetryCatalog.ts
85
+ var telemetryCatalogVersion = 1;
86
+ var TELEMETRY_EVENT_CATALOG = [
87
+ {
88
+ name: "course_started",
89
+ description: "Learner session began for a course",
90
+ requiredFields: ["courseId", "sessionId", "timestamp"],
91
+ dataFields: [],
92
+ xapiVerb: "http://adlnet.gov/expapi/verbs/initialized",
93
+ urnPattern: "urn:lessonkit:course:{courseId}"
94
+ },
95
+ {
96
+ name: "course_completed",
97
+ description: "Course completion criteria met",
98
+ requiredFields: ["courseId", "sessionId", "timestamp"],
99
+ dataFields: [],
100
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
101
+ urnPattern: "urn:lessonkit:course:{courseId}"
102
+ },
103
+ {
104
+ name: "lesson_started",
105
+ description: "Lesson became active",
106
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
107
+ dataFields: ["lessonId"],
108
+ xapiVerb: "http://adlnet.gov/expapi/verbs/initialized",
109
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
110
+ },
111
+ {
112
+ name: "lesson_completed",
113
+ description: "Lesson marked complete",
114
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
115
+ dataFields: ["lessonId", "durationMs"],
116
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
117
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
118
+ },
119
+ {
120
+ name: "lesson_time_on_task",
121
+ description: "Time on task for a lesson (companion to lesson_completed)",
122
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
123
+ dataFields: ["lessonId", "durationMs"],
124
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
125
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
126
+ },
127
+ {
128
+ name: "quiz_answered",
129
+ description: "Learner submitted a quiz/check answer",
130
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
131
+ dataFields: ["checkId", "question", "choice", "correct"],
132
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
133
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
134
+ },
135
+ {
136
+ name: "quiz_completed",
137
+ description: "Quiz/check completed (e.g. first correct answer)",
138
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
139
+ dataFields: ["checkId", "score", "maxScore"],
140
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
141
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
142
+ },
143
+ {
144
+ name: "interaction",
145
+ description: "Custom interaction (branching, UI actions, blocks)",
146
+ requiredFields: ["courseId", "sessionId", "timestamp"],
147
+ dataFields: ["kind", "blockId", "payload"],
148
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
149
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
150
+ }
151
+ ];
152
+ function buildTelemetryCatalog() {
153
+ return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
154
+ }
155
+
1
156
  // src/trackingClient.ts
2
157
  function createTrackingClient(opts) {
3
158
  const sink = opts?.sink;
@@ -81,7 +236,17 @@ function nowIso() {
81
236
  return (/* @__PURE__ */ new Date()).toISOString();
82
237
  }
83
238
  export {
239
+ ID_MAX_LENGTH,
240
+ ID_PATTERN,
241
+ TELEMETRY_EVENT_CATALOG,
242
+ assertValidId,
243
+ buildLessonkitUrn,
244
+ buildTelemetryCatalog,
84
245
  createSessionId,
85
246
  createTrackingClient,
86
- nowIso
247
+ deriveId,
248
+ nowIso,
249
+ slugifyId,
250
+ telemetryCatalogVersion,
251
+ validateId
87
252
  };
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lessonkit.dev/schemas/identity-contract.v1.json",
4
+ "title": "LessonKit Identity Contract v1",
5
+ "description": "Stable id format and URN patterns for LessonKit courses, lessons, checks, and blocks.",
6
+ "schemaVersion": 1,
7
+ "idPattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
8
+ "maxLength": 64,
9
+ "requiredComponentIds": {
10
+ "Course": ["courseId"],
11
+ "Lesson": ["lessonId"],
12
+ "Quiz": ["checkId"],
13
+ "KnowledgeCheck": ["checkId"]
14
+ },
15
+ "optionalComponentIds": {
16
+ "Scenario": ["blockId"],
17
+ "Reflection": ["blockId"]
18
+ },
19
+ "urnPatterns": {
20
+ "course": "urn:lessonkit:course:{courseId}",
21
+ "lesson": "urn:lessonkit:course:{courseId}:lesson:{lessonId}",
22
+ "check": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}",
23
+ "block": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -29,10 +29,14 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js",
31
31
  "require": "./dist/index.cjs"
32
- }
32
+ },
33
+ "./telemetry-catalog.v1.json": "./telemetry-catalog.v1.json",
34
+ "./identity-contract.v1.json": "./identity-contract.v1.json"
33
35
  },
34
36
  "files": [
35
- "dist"
37
+ "dist",
38
+ "telemetry-catalog.v1.json",
39
+ "identity-contract.v1.json"
36
40
  ],
37
41
  "scripts": {
38
42
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -0,0 +1,69 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "entries": [
4
+ {
5
+ "name": "course_started",
6
+ "description": "Learner session began for a course",
7
+ "requiredFields": ["courseId", "sessionId", "timestamp"],
8
+ "dataFields": [],
9
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/initialized",
10
+ "urnPattern": "urn:lessonkit:course:{courseId}"
11
+ },
12
+ {
13
+ "name": "course_completed",
14
+ "description": "Course completion criteria met",
15
+ "requiredFields": ["courseId", "sessionId", "timestamp"],
16
+ "dataFields": [],
17
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/completed",
18
+ "urnPattern": "urn:lessonkit:course:{courseId}"
19
+ },
20
+ {
21
+ "name": "lesson_started",
22
+ "description": "Lesson became active",
23
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
24
+ "dataFields": ["lessonId"],
25
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/initialized",
26
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
27
+ },
28
+ {
29
+ "name": "lesson_completed",
30
+ "description": "Lesson marked complete",
31
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
32
+ "dataFields": ["lessonId", "durationMs"],
33
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/completed",
34
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
35
+ },
36
+ {
37
+ "name": "lesson_time_on_task",
38
+ "description": "Time on task for a lesson (companion to lesson_completed)",
39
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
40
+ "dataFields": ["lessonId", "durationMs"],
41
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/completed",
42
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}"
43
+ },
44
+ {
45
+ "name": "quiz_answered",
46
+ "description": "Learner submitted a quiz/check answer",
47
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
48
+ "dataFields": ["checkId", "question", "choice", "correct"],
49
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/answered",
50
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
51
+ },
52
+ {
53
+ "name": "quiz_completed",
54
+ "description": "Quiz/check completed (e.g. first correct answer)",
55
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
56
+ "dataFields": ["checkId", "score", "maxScore"],
57
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/completed",
58
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
59
+ },
60
+ {
61
+ "name": "interaction",
62
+ "description": "Custom interaction (branching, UI actions, blocks)",
63
+ "requiredFields": ["courseId", "sessionId", "timestamp"],
64
+ "dataFields": ["kind", "blockId", "payload"],
65
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/experienced",
66
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
67
+ }
68
+ ]
69
+ }