@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 +6 -3
- package/dist/index.cjs +177 -2
- package/dist/index.d.cts +109 -6
- package/dist/index.d.ts +109 -6
- package/dist/index.js +166 -1
- package/identity-contract.v1.json +25 -0
- package/package.json +7 -3
- package/telemetry-catalog.v1.json +69 -0
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.
|
|
15
|
+
## What’s inside (0.5.0)
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
name: TelemetryEventName;
|
|
47
|
+
type TelemetryEventBase = {
|
|
12
48
|
timestamp: string;
|
|
13
|
-
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
|
|
11
|
-
name: TelemetryEventName;
|
|
47
|
+
type TelemetryEventBase = {
|
|
12
48
|
timestamp: string;
|
|
13
|
-
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
|
-
|
|
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
|
+
"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
|
+
}
|