@intellectif/lk-core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-zdcAMtFB.d.cts +626 -0
- package/dist/activity-zdcAMtFB.d.ts +626 -0
- package/dist/chunk-3YAVDV5F.js +47 -0
- package/dist/chunk-3YAVDV5F.js.map +1 -0
- package/dist/chunk-55O4M45K.js +57 -0
- package/dist/chunk-55O4M45K.js.map +1 -0
- package/dist/chunk-5GJJHGY5.js +576 -0
- package/dist/chunk-5GJJHGY5.js.map +1 -0
- package/dist/{chunk-ZHOX6TO4.js → chunk-HS7BYCGE.js} +65 -7
- package/dist/chunk-HS7BYCGE.js.map +1 -0
- package/dist/chunk-PIMX4B4D.cjs +47 -0
- package/dist/chunk-PIMX4B4D.cjs.map +1 -0
- package/dist/chunk-QOAORTA4.cjs +576 -0
- package/dist/chunk-QOAORTA4.cjs.map +1 -0
- package/dist/chunk-QSVBYTNM.js +94 -0
- package/dist/chunk-QSVBYTNM.js.map +1 -0
- package/dist/{chunk-PJFDAP54.cjs → chunk-RUGIOQZY.cjs} +66 -8
- package/dist/chunk-RUGIOQZY.cjs.map +1 -0
- package/dist/chunk-THSZMZND.cjs +94 -0
- package/dist/chunk-THSZMZND.cjs.map +1 -0
- package/dist/chunk-YNTYWHZI.cjs +57 -0
- package/dist/chunk-YNTYWHZI.cjs.map +1 -0
- package/dist/index.cjs +185 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +253 -7
- package/dist/index.d.ts +253 -7
- package/dist/index.js +188 -9
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +29 -2
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +312 -31
- package/dist/schemas.d.ts +312 -31
- package/dist/schemas.js +32 -5
- package/dist/scoring.cjs +13 -3
- package/dist/scoring.cjs.map +1 -1
- package/dist/scoring.d.cts +29 -5
- package/dist/scoring.d.ts +29 -5
- package/dist/scoring.js +12 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +40 -2
- package/dist/xapi.d.ts +40 -2
- package/dist/xapi.js +2 -2
- package/package.json +7 -2
- package/dist/activity-D5tqgx8A.d.cts +0 -330
- package/dist/activity-D5tqgx8A.d.ts +0 -330
- package/dist/chunk-2T3IL7VL.js +0 -147
- package/dist/chunk-2T3IL7VL.js.map +0 -1
- package/dist/chunk-3JMLUUFS.cjs +0 -124
- package/dist/chunk-3JMLUUFS.cjs.map +0 -1
- package/dist/chunk-6VOCV4EX.js +0 -25
- package/dist/chunk-6VOCV4EX.js.map +0 -1
- package/dist/chunk-DCDOTQNH.cjs +0 -147
- package/dist/chunk-DCDOTQNH.cjs.map +0 -1
- package/dist/chunk-DV466SBJ.cjs +0 -25
- package/dist/chunk-DV466SBJ.cjs.map +0 -1
- package/dist/chunk-GZ73G2TS.js +0 -124
- package/dist/chunk-GZ73G2TS.js.map +0 -1
- package/dist/chunk-PJFDAP54.cjs.map +0 -1
- package/dist/chunk-ZHOX6TO4.js.map +0 -1
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An xAPI Activity object — the thing a statement is about.
|
|
3
|
-
* This SDK only builds Activity-type objects (xAPI 1.0.3 §4.1.4.1).
|
|
4
|
-
*/
|
|
5
|
-
interface XAPIObject {
|
|
6
|
-
objectType: 'Activity';
|
|
7
|
-
/** IRI uniquely identifying the activity. */
|
|
8
|
-
id: string;
|
|
9
|
-
definition?: {
|
|
10
|
-
/** Language-map of human-readable names for the activity. */
|
|
11
|
-
name?: Record<string, string>;
|
|
12
|
-
/** Language-map of human-readable descriptions. */
|
|
13
|
-
description?: Record<string, string>;
|
|
14
|
-
/** IRI identifying the activity type. */
|
|
15
|
-
type?: string;
|
|
16
|
-
extensions?: Record<string, unknown>;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* An xAPI Agent (person or system) that performed the statement's verb.
|
|
21
|
-
* Exactly one of `mbox` or `account` must be present.
|
|
22
|
-
*/
|
|
23
|
-
interface XAPIActor {
|
|
24
|
-
objectType: 'Agent';
|
|
25
|
-
/** Display name for the actor. */
|
|
26
|
-
name?: string;
|
|
27
|
-
/** Mailto IRI uniquely identifying the actor (e.g. `mailto:user@example.com`). */
|
|
28
|
-
mbox?: string;
|
|
29
|
-
/** Account-based actor identifier. */
|
|
30
|
-
account?: {
|
|
31
|
-
/** IRI of the home page of the account's service provider. */
|
|
32
|
-
homePage: string;
|
|
33
|
-
/** The actor's unique name on that service. */
|
|
34
|
-
name: string;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/** An xAPI Verb — the action performed in the statement. */
|
|
38
|
-
interface XAPIVerbObject {
|
|
39
|
-
/** IRI uniquely identifying this verb (e.g. from adlnet.gov/expapi/verbs). */
|
|
40
|
-
id: string;
|
|
41
|
-
/** Language-map of human-readable verb labels. */
|
|
42
|
-
display: Record<string, string>;
|
|
43
|
-
}
|
|
44
|
-
/** Score sub-object within an xAPI Result. */
|
|
45
|
-
interface XAPIScore {
|
|
46
|
-
/** Normalized score in the range [0, 1]. */
|
|
47
|
-
scaled: number;
|
|
48
|
-
/** Raw score in the instrument's native scale. */
|
|
49
|
-
raw?: number;
|
|
50
|
-
/** Minimum possible raw score. */
|
|
51
|
-
min?: number;
|
|
52
|
-
/** Maximum possible raw score. */
|
|
53
|
-
max?: number;
|
|
54
|
-
}
|
|
55
|
-
/** Result information attached to an xAPI Statement. */
|
|
56
|
-
interface XAPIResult {
|
|
57
|
-
score?: XAPIScore;
|
|
58
|
-
/** Whether the actor's performance was successful. */
|
|
59
|
-
success?: boolean;
|
|
60
|
-
/** Whether the activity was completed. */
|
|
61
|
-
completion?: boolean;
|
|
62
|
-
/** ISO 8601 duration string representing time spent (e.g. `PT5M30S`). */
|
|
63
|
-
duration?: string;
|
|
64
|
-
/** A string representation of the learner's response. */
|
|
65
|
-
response?: string;
|
|
66
|
-
extensions?: Record<string, unknown>;
|
|
67
|
-
}
|
|
68
|
-
/** Context information attached to an xAPI Statement. */
|
|
69
|
-
interface XAPIContext {
|
|
70
|
-
/** Name of the software used to record the statement. */
|
|
71
|
-
platform?: string;
|
|
72
|
-
/** BCP 47 language tag for the activity content. */
|
|
73
|
-
language?: string;
|
|
74
|
-
extensions?: Record<string, unknown>;
|
|
75
|
-
}
|
|
76
|
-
/** A complete xAPI 1.0.3 Statement. */
|
|
77
|
-
interface XAPIStatement {
|
|
78
|
-
/** UUID v4 uniquely identifying this statement. */
|
|
79
|
-
id: string;
|
|
80
|
-
actor: XAPIActor;
|
|
81
|
-
verb: XAPIVerbObject;
|
|
82
|
-
object: XAPIObject;
|
|
83
|
-
result?: XAPIResult;
|
|
84
|
-
context?: XAPIContext;
|
|
85
|
-
/** ISO 8601 timestamp of when the statement occurred. */
|
|
86
|
-
timestamp: string;
|
|
87
|
-
version: '1.0.3';
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Configuration passed to `useXAPI` to connect to an LRS endpoint.
|
|
91
|
-
* The `auth` field supports both HTTP Basic and Bearer token authentication.
|
|
92
|
-
*/
|
|
93
|
-
interface XAPIConfig {
|
|
94
|
-
/** Full URL of the LRS statements endpoint. */
|
|
95
|
-
endpoint: string;
|
|
96
|
-
/** Authentication credentials for the LRS. */
|
|
97
|
-
auth: {
|
|
98
|
-
type: 'basic';
|
|
99
|
-
username: string;
|
|
100
|
-
password: string;
|
|
101
|
-
} | {
|
|
102
|
-
type: 'bearer';
|
|
103
|
-
token: string;
|
|
104
|
-
};
|
|
105
|
-
/** IRI identifying the activity in all statements sent by this hook instance. */
|
|
106
|
-
activityId: string;
|
|
107
|
-
actor: XAPIActor;
|
|
108
|
-
/** Called after all retry attempts are exhausted. */
|
|
109
|
-
onError?: (err: XAPIError) => void;
|
|
110
|
-
}
|
|
111
|
-
/** Error payload delivered to `XAPIConfig.onError` after retries are exhausted. */
|
|
112
|
-
interface XAPIError {
|
|
113
|
-
/** The statement that failed to send. */
|
|
114
|
-
statement: XAPIStatement;
|
|
115
|
-
/** The 1-based attempt number on which the final failure occurred. */
|
|
116
|
-
attempt: number;
|
|
117
|
-
/** HTTP status code from the LRS, if a response was received. */
|
|
118
|
-
statusCode?: number;
|
|
119
|
-
/** Human-readable error description. */
|
|
120
|
-
message: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** The set of activity types supported by learning-kit. */
|
|
124
|
-
type ActivityType = 'multiple-choice' | 'fill-in-the-blanks';
|
|
125
|
-
/** Maps each ActivityType string to its corresponding data shape. */
|
|
126
|
-
interface ActivityDataMap {
|
|
127
|
-
'multiple-choice': MultipleChoiceData;
|
|
128
|
-
'fill-in-the-blanks': FillInTheBlanksData;
|
|
129
|
-
}
|
|
130
|
-
/** Union of all valid activity data shapes. */
|
|
131
|
-
type ActivityData = MultipleChoiceData | FillInTheBlanksData;
|
|
132
|
-
/**
|
|
133
|
-
* Optional media (image/audio/video) shown above a question or passage.
|
|
134
|
-
* URL-only: hosting/delivery is the consuming application's responsibility.
|
|
135
|
-
*/
|
|
136
|
-
interface ActivityMedia {
|
|
137
|
-
/** The kind of media; selects the rendered element. `embed` → sandboxed iframe. */
|
|
138
|
-
type: 'image' | 'audio' | 'video' | 'embed';
|
|
139
|
-
/**
|
|
140
|
-
* Source URL. For `embed` this MUST be the provider's embeddable URL
|
|
141
|
-
* (e.g. `https://www.youtube.com/embed/<id>`), not the watch page.
|
|
142
|
-
*/
|
|
143
|
-
url: string;
|
|
144
|
-
/** Alternative text. Required for `image` and `embed`; optional label for audio/video. */
|
|
145
|
-
alt?: string;
|
|
146
|
-
/** Optional WebVTT captions track URL for `audio`/`video`. */
|
|
147
|
-
captionsUrl?: string;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Optional authored "overall feedback" shown after submission, chosen by
|
|
151
|
-
* whether the learner passed (h5p-style overall feedback). Distinct from
|
|
152
|
-
* per-option feedback; either field may be omitted.
|
|
153
|
-
*/
|
|
154
|
-
interface ActivityFeedback {
|
|
155
|
-
/** Shown when the learner passes (score ≥ pass threshold). */
|
|
156
|
-
correct?: string;
|
|
157
|
-
/** Shown when the learner does not pass. */
|
|
158
|
-
incorrect?: string;
|
|
159
|
-
}
|
|
160
|
-
/** A single selectable option within a Multiple Choice activity. */
|
|
161
|
-
interface MultipleChoiceOption {
|
|
162
|
-
/** Unique identifier for this option within the activity. */
|
|
163
|
-
id: string;
|
|
164
|
-
/** Display text shown to the learner. */
|
|
165
|
-
text: string;
|
|
166
|
-
/** Whether this option is part of the correct answer. */
|
|
167
|
-
isCorrect: boolean;
|
|
168
|
-
/** Optional per-option feedback shown after submission. */
|
|
169
|
-
feedback?: string;
|
|
170
|
-
}
|
|
171
|
-
/** Data contract for a Multiple Choice activity. */
|
|
172
|
-
interface MultipleChoiceData {
|
|
173
|
-
schemaVersion: '1.0';
|
|
174
|
-
type: 'multiple-choice';
|
|
175
|
-
/** Unique identifier for this activity. */
|
|
176
|
-
id: string;
|
|
177
|
-
/** Human-readable title used in xAPI statements and error boundaries. */
|
|
178
|
-
title: string;
|
|
179
|
-
/** The question stem presented to the learner. */
|
|
180
|
-
question: string;
|
|
181
|
-
/** `single` allows one selection; `multi` allows multiple. */
|
|
182
|
-
mode: 'single' | 'multi';
|
|
183
|
-
/** Ordered list of answer options. */
|
|
184
|
-
options: MultipleChoiceOption[];
|
|
185
|
-
/** Scoring algorithm applied when the learner submits. */
|
|
186
|
-
scoringStrategy: 'all-or-nothing' | 'partial';
|
|
187
|
-
/** Optional media shown above the question. */
|
|
188
|
-
media?: ActivityMedia;
|
|
189
|
-
/** Optional authored overall feedback shown after submission. */
|
|
190
|
-
feedback?: ActivityFeedback;
|
|
191
|
-
/** Minimum scaled score [0–1] required to pass. Defaults to 0.6 when absent. */
|
|
192
|
-
passThreshold?: number;
|
|
193
|
-
/** When true, options are shuffled deterministically per session. */
|
|
194
|
-
shuffle?: boolean;
|
|
195
|
-
/** BCP 47 language tag for the activity content. */
|
|
196
|
-
locale?: string;
|
|
197
|
-
/** IRI references to learning objectives addressed by this activity. */
|
|
198
|
-
learningObjectives?: string[];
|
|
199
|
-
/** Subjective difficulty on a 1–5 scale. */
|
|
200
|
-
difficultyLevel?: 1 | 2 | 3 | 4 | 5;
|
|
201
|
-
}
|
|
202
|
-
/** Configuration for a single fill-in-the-blank slot. */
|
|
203
|
-
interface BlankConfig {
|
|
204
|
-
/** Unique identifier matching the `{{blank_id}}` placeholder in the passage. */
|
|
205
|
-
id: string;
|
|
206
|
-
/** List of strings accepted as correct answers for this blank. */
|
|
207
|
-
acceptedAnswers: string[];
|
|
208
|
-
/** Whether answer matching is case-sensitive. Defaults to false. */
|
|
209
|
-
caseSensitive?: boolean;
|
|
210
|
-
/** Whether leading/trailing whitespace is stripped before matching. Defaults to true. */
|
|
211
|
-
trimWhitespace?: boolean;
|
|
212
|
-
/** Optional hint text revealed on learner request. */
|
|
213
|
-
hint?: string;
|
|
214
|
-
/** Optional feedback shown inline next to this blank after submission. */
|
|
215
|
-
feedback?: string;
|
|
216
|
-
}
|
|
217
|
-
/** Data contract for a Fill-in-the-Blanks activity. */
|
|
218
|
-
interface FillInTheBlanksData {
|
|
219
|
-
schemaVersion: '1.0';
|
|
220
|
-
type: 'fill-in-the-blanks';
|
|
221
|
-
/** Unique identifier for this activity. */
|
|
222
|
-
id: string;
|
|
223
|
-
/** Human-readable title used in xAPI statements and error boundaries. */
|
|
224
|
-
title: string;
|
|
225
|
-
/** Passage text containing `{{blank_id}}` placeholders. */
|
|
226
|
-
passage: string;
|
|
227
|
-
/** Configuration for each blank in the passage. */
|
|
228
|
-
blanks: BlankConfig[];
|
|
229
|
-
/** Scoring algorithm applied when the learner submits. */
|
|
230
|
-
scoringStrategy: 'all-or-nothing' | 'partial';
|
|
231
|
-
/** Optional media shown above the passage. */
|
|
232
|
-
media?: ActivityMedia;
|
|
233
|
-
/** Optional authored overall feedback shown after submission. */
|
|
234
|
-
feedback?: ActivityFeedback;
|
|
235
|
-
/** Minimum scaled score [0–1] required to pass. Defaults to 0.6 when absent. */
|
|
236
|
-
passThreshold?: number;
|
|
237
|
-
/** BCP 47 language tag for the activity content. */
|
|
238
|
-
locale?: string;
|
|
239
|
-
/** IRI references to learning objectives addressed by this activity. */
|
|
240
|
-
learningObjectives?: string[];
|
|
241
|
-
/** Subjective difficulty on a 1–5 scale. */
|
|
242
|
-
difficultyLevel?: 1 | 2 | 3 | 4 | 5;
|
|
243
|
-
}
|
|
244
|
-
/** Union of all learner response shapes. */
|
|
245
|
-
type LearnerResponse = MultipleChoiceLearnerResponse | FillInTheBlanksLearnerResponse;
|
|
246
|
-
/** Learner response for a Multiple Choice activity. */
|
|
247
|
-
interface MultipleChoiceLearnerResponse {
|
|
248
|
-
type: 'multiple-choice';
|
|
249
|
-
/** IDs of the options the learner selected. */
|
|
250
|
-
selectedOptionIds: string[];
|
|
251
|
-
}
|
|
252
|
-
/** Learner response for a Fill-in-the-Blanks activity. */
|
|
253
|
-
interface FillInTheBlanksLearnerResponse {
|
|
254
|
-
type: 'fill-in-the-blanks';
|
|
255
|
-
/** Map of blank ID to the learner's typed answer. */
|
|
256
|
-
answers: Record<string, string>;
|
|
257
|
-
}
|
|
258
|
-
/** Per-item scoring breakdown returned by the scoring engine. */
|
|
259
|
-
interface ScoringDetail {
|
|
260
|
-
/** ID of the option or blank this detail refers to. */
|
|
261
|
-
itemId: string;
|
|
262
|
-
/** Whether the learner's response for this item was correct. */
|
|
263
|
-
correct: boolean;
|
|
264
|
-
/** The learner's actual response for this item. */
|
|
265
|
-
learnerResponse: string | string[];
|
|
266
|
-
/** The expected correct response(s) for this item. */
|
|
267
|
-
correctResponse: string | string[];
|
|
268
|
-
/** Optional weight applied to this item's contribution to the overall score. */
|
|
269
|
-
weight?: number;
|
|
270
|
-
}
|
|
271
|
-
/** Full scoring result returned by the scoring engine. */
|
|
272
|
-
interface ScoringResult {
|
|
273
|
-
/** Scaled score in the range [0, 1]. */
|
|
274
|
-
score: number;
|
|
275
|
-
/** Maximum possible scaled score (always 1). */
|
|
276
|
-
maxScore: number;
|
|
277
|
-
/** Whether the score meets or exceeds the activity's pass threshold. */
|
|
278
|
-
passed: boolean;
|
|
279
|
-
/** Optional top-level feedback message, or null when absent. */
|
|
280
|
-
feedback: string | null;
|
|
281
|
-
/** Per-item scoring breakdown. */
|
|
282
|
-
details: ScoringDetail[];
|
|
283
|
-
}
|
|
284
|
-
/** A single validation error produced by `validateActivity`. */
|
|
285
|
-
interface ValidationError {
|
|
286
|
-
/** JSON-path-style location of the invalid field. */
|
|
287
|
-
path: string[];
|
|
288
|
-
/** Human-readable description of the validation failure. */
|
|
289
|
-
message: string;
|
|
290
|
-
/** Machine-readable error code. */
|
|
291
|
-
code: string;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Result of a `validateActivity` call.
|
|
295
|
-
* On success, `data` is the validated and typed activity data.
|
|
296
|
-
* On failure, `errors` contains one entry per violated constraint.
|
|
297
|
-
*/
|
|
298
|
-
type ValidationResult<T> = {
|
|
299
|
-
success: true;
|
|
300
|
-
data: T;
|
|
301
|
-
} | {
|
|
302
|
-
success: false;
|
|
303
|
-
errors: ValidationError[];
|
|
304
|
-
};
|
|
305
|
-
/** The payload delivered to an activity's `onComplete` callback. */
|
|
306
|
-
interface ActivityResult {
|
|
307
|
-
/** Scaled score in the range [0, 1]. */
|
|
308
|
-
score: number;
|
|
309
|
-
/** Maximum possible scaled score (always 1). */
|
|
310
|
-
maxScore: number;
|
|
311
|
-
/** Whether the learner passed based on the activity's pass threshold. */
|
|
312
|
-
passed: boolean;
|
|
313
|
-
/** Time in milliseconds from first interaction to submission. */
|
|
314
|
-
timeSpent: number;
|
|
315
|
-
/** The xAPI statement built and (optionally) sent for this attempt. */
|
|
316
|
-
xapiStatement: XAPIStatement;
|
|
317
|
-
}
|
|
318
|
-
/** Fired by activity components on every discrete learner interaction. */
|
|
319
|
-
interface InteractionEvent {
|
|
320
|
-
/** The kind of interaction that occurred. */
|
|
321
|
-
type: 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'submitted';
|
|
322
|
-
/** ID of the activity that produced the event. */
|
|
323
|
-
activityId: string;
|
|
324
|
-
/** Unix timestamp (ms) of when the interaction occurred. */
|
|
325
|
-
timestamp: number;
|
|
326
|
-
/** Additional event-specific data. */
|
|
327
|
-
payload: Record<string, unknown>;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export type { ActivityData as A, BlankConfig as B, FillInTheBlanksData as F, InteractionEvent as I, LearnerResponse as L, MultipleChoiceData as M, ScoringDetail as S, ValidationError as V, XAPIActor as X, ActivityDataMap as a, ActivityFeedback as b, ActivityMedia as c, ActivityResult as d, ActivityType as e, FillInTheBlanksLearnerResponse as f, MultipleChoiceLearnerResponse as g, MultipleChoiceOption as h, ScoringResult as i, ValidationResult as j, XAPIConfig as k, XAPIContext as l, XAPIError as m, XAPIObject as n, XAPIResult as o, XAPIScore as p, XAPIStatement as q, XAPIVerbObject as r };
|
package/dist/chunk-2T3IL7VL.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// src/schemas/fill-in-the-blanks.ts
|
|
2
|
-
import { z as z3 } from "zod/v4";
|
|
3
|
-
|
|
4
|
-
// src/schemas/feedback.ts
|
|
5
|
-
import { z } from "zod/v4";
|
|
6
|
-
var FeedbackSchema = z.object({
|
|
7
|
-
correct: z.string().min(1).optional(),
|
|
8
|
-
incorrect: z.string().min(1).optional()
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// src/schemas/media.ts
|
|
12
|
-
import { z as z2 } from "zod/v4";
|
|
13
|
-
var MediaSchema = z2.object({
|
|
14
|
-
type: z2.enum(["image", "audio", "video", "embed"]),
|
|
15
|
-
url: z2.url(),
|
|
16
|
-
alt: z2.string().min(1).optional(),
|
|
17
|
-
captionsUrl: z2.url().optional()
|
|
18
|
-
}).refine(
|
|
19
|
-
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
20
|
-
{
|
|
21
|
-
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
22
|
-
path: ["alt"]
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
// src/schemas/fill-in-the-blanks.ts
|
|
27
|
-
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
28
|
-
var BlankConfigSchema = z3.object({
|
|
29
|
-
id: z3.string().min(1),
|
|
30
|
-
acceptedAnswers: z3.array(z3.string().min(1)).min(1),
|
|
31
|
-
caseSensitive: z3.boolean().optional(),
|
|
32
|
-
trimWhitespace: z3.boolean().optional(),
|
|
33
|
-
hint: z3.string().optional(),
|
|
34
|
-
feedback: z3.string().optional()
|
|
35
|
-
});
|
|
36
|
-
var FillInTheBlanksDataSchema = z3.object({
|
|
37
|
-
schemaVersion: z3.literal("1.0"),
|
|
38
|
-
type: z3.literal("fill-in-the-blanks"),
|
|
39
|
-
id: z3.string().min(1),
|
|
40
|
-
title: z3.string().min(1),
|
|
41
|
-
passage: z3.string().min(1),
|
|
42
|
-
blanks: z3.array(BlankConfigSchema).min(1),
|
|
43
|
-
scoringStrategy: z3.enum(["all-or-nothing", "partial"]),
|
|
44
|
-
media: MediaSchema.optional(),
|
|
45
|
-
feedback: FeedbackSchema.optional(),
|
|
46
|
-
passThreshold: z3.number().min(0).max(1).optional(),
|
|
47
|
-
locale: z3.string().optional(),
|
|
48
|
-
learningObjectives: z3.array(z3.string()).optional(),
|
|
49
|
-
difficultyLevel: z3.literal([1, 2, 3, 4, 5]).optional()
|
|
50
|
-
}).refine(
|
|
51
|
-
(data) => {
|
|
52
|
-
const placeholderIds = new Set(
|
|
53
|
-
[...data.passage.matchAll(PLACEHOLDER_RE)].map((match) => match[1])
|
|
54
|
-
);
|
|
55
|
-
const blankIds = new Set(data.blanks.map((blank) => blank.id));
|
|
56
|
-
if (placeholderIds.size !== blankIds.size) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
for (const id of blankIds) {
|
|
60
|
-
if (!placeholderIds.has(id)) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return true;
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
error: "Each blank id must have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
68
|
-
path: ["passage"]
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// src/schemas/multiple-choice.ts
|
|
73
|
-
import { z as z4 } from "zod/v4";
|
|
74
|
-
var MultipleChoiceOptionSchema = z4.object({
|
|
75
|
-
id: z4.string().min(1),
|
|
76
|
-
text: z4.string().min(1),
|
|
77
|
-
isCorrect: z4.boolean(),
|
|
78
|
-
feedback: z4.string().optional()
|
|
79
|
-
});
|
|
80
|
-
var MultipleChoiceDataSchema = z4.object({
|
|
81
|
-
schemaVersion: z4.literal("1.0"),
|
|
82
|
-
type: z4.literal("multiple-choice"),
|
|
83
|
-
id: z4.string().min(1),
|
|
84
|
-
title: z4.string().min(1),
|
|
85
|
-
question: z4.string().min(1),
|
|
86
|
-
mode: z4.enum(["single", "multi"]),
|
|
87
|
-
options: z4.array(MultipleChoiceOptionSchema).min(2).max(10),
|
|
88
|
-
scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
|
|
89
|
-
media: MediaSchema.optional(),
|
|
90
|
-
feedback: FeedbackSchema.optional(),
|
|
91
|
-
passThreshold: z4.number().min(0).max(1).optional(),
|
|
92
|
-
shuffle: z4.boolean().optional(),
|
|
93
|
-
locale: z4.string().optional(),
|
|
94
|
-
learningObjectives: z4.array(z4.string()).optional(),
|
|
95
|
-
difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
|
|
96
|
-
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
97
|
-
error: "At least one option must be marked correct.",
|
|
98
|
-
path: ["options"]
|
|
99
|
-
}).refine(
|
|
100
|
-
(data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
|
|
101
|
-
{
|
|
102
|
-
error: 'Single-select activities (mode: "single") must have exactly one correct option.',
|
|
103
|
-
path: ["options"]
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// src/schemas/json-schema.ts
|
|
108
|
-
import { z as z5 } from "zod/v4";
|
|
109
|
-
var multipleChoiceJsonSchema = z5.toJSONSchema(MultipleChoiceDataSchema, {
|
|
110
|
-
target: "draft-7"
|
|
111
|
-
});
|
|
112
|
-
var fillInTheBlanksJsonSchema = z5.toJSONSchema(FillInTheBlanksDataSchema, {
|
|
113
|
-
target: "draft-7"
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// src/schemas/index.ts
|
|
117
|
-
var schemaMap = {
|
|
118
|
-
"multiple-choice": MultipleChoiceDataSchema,
|
|
119
|
-
"fill-in-the-blanks": FillInTheBlanksDataSchema
|
|
120
|
-
};
|
|
121
|
-
function validateActivity(type, data) {
|
|
122
|
-
const result = schemaMap[type].safeParse(data);
|
|
123
|
-
if (result.success) {
|
|
124
|
-
return { success: true, data: result.data };
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
success: false,
|
|
128
|
-
errors: result.error.issues.map((issue) => ({
|
|
129
|
-
path: issue.path.map(String),
|
|
130
|
-
message: issue.message,
|
|
131
|
-
code: issue.code
|
|
132
|
-
}))
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export {
|
|
137
|
-
FeedbackSchema,
|
|
138
|
-
MediaSchema,
|
|
139
|
-
BlankConfigSchema,
|
|
140
|
-
FillInTheBlanksDataSchema,
|
|
141
|
-
MultipleChoiceOptionSchema,
|
|
142
|
-
MultipleChoiceDataSchema,
|
|
143
|
-
multipleChoiceJsonSchema,
|
|
144
|
-
fillInTheBlanksJsonSchema,
|
|
145
|
-
validateActivity
|
|
146
|
-
};
|
|
147
|
-
//# sourceMappingURL=chunk-2T3IL7VL.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemas/fill-in-the-blanks.ts","../src/schemas/feedback.ts","../src/schemas/media.ts","../src/schemas/multiple-choice.ts","../src/schemas/json-schema.ts","../src/schemas/index.ts"],"sourcesContent":["import { z } from 'zod/v4';\nimport { FeedbackSchema } from './feedback.js';\nimport { MediaSchema } from './media.js';\n\n/** Matches `{{ blank_id }}` placeholders in a passage, capturing the trimmed id. */\nconst PLACEHOLDER_RE = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\n/** Zod schema for a single fill-in-the-blank slot configuration. */\nexport const BlankConfigSchema = z.object({\n id: z.string().min(1),\n acceptedAnswers: z.array(z.string().min(1)).min(1),\n caseSensitive: z.boolean().optional(),\n trimWhitespace: z.boolean().optional(),\n hint: z.string().optional(),\n feedback: z.string().optional(),\n});\n\n/**\n * Zod schema validating the full Fill-in-the-Blanks activity data contract.\n *\n * The refinement enforces a bijection between `{{id}}` placeholders in the\n * passage and `blanks[].id`; otherwise a blank could never be rendered, or a\n * placeholder could have no scoring config, yielding undefined behaviour.\n */\nexport const FillInTheBlanksDataSchema = z\n .object({\n schemaVersion: z.literal('1.0'),\n type: z.literal('fill-in-the-blanks'),\n id: z.string().min(1),\n title: z.string().min(1),\n passage: z.string().min(1),\n blanks: z.array(BlankConfigSchema).min(1),\n scoringStrategy: z.enum(['all-or-nothing', 'partial']),\n media: MediaSchema.optional(),\n feedback: FeedbackSchema.optional(),\n passThreshold: z.number().min(0).max(1).optional(),\n locale: z.string().optional(),\n learningObjectives: z.array(z.string()).optional(),\n difficultyLevel: z.literal([1, 2, 3, 4, 5]).optional(),\n })\n .refine(\n (data) => {\n const placeholderIds = new Set(\n [...data.passage.matchAll(PLACEHOLDER_RE)].map((match) => match[1]),\n );\n const blankIds = new Set(data.blanks.map((blank) => blank.id));\n if (placeholderIds.size !== blankIds.size) {\n return false;\n }\n for (const id of blankIds) {\n if (!placeholderIds.has(id)) {\n return false;\n }\n }\n return true;\n },\n {\n error:\n 'Each blank id must have exactly one matching {{id}} placeholder in the passage, and vice versa.',\n path: ['passage'],\n },\n );\n","import { z } from 'zod/v4';\n\n/**\n * Optional authored \"overall feedback\" shown after submission, selected by\n * whether the learner passed (h5p-style overall feedback). Both fields are\n * optional; non-empty when present.\n */\nexport const FeedbackSchema = z.object({\n correct: z.string().min(1).optional(),\n incorrect: z.string().min(1).optional(),\n});\n","import { z } from 'zod/v4';\n\n/**\n * Optional media attached to an activity, rendered above the question or\n * passage — e.g. a recording to listen to, or an embedded video to watch\n * before answering. URL-only by design: hosting/delivery (S3/CDN, or the\n * provider's own embed for `embed`) is the consuming app's responsibility\n * (see requirements \"Non-Goals and Shared Responsibility\").\n *\n * - `image`: rendered as `<img>` — `alt` is REQUIRED (WCAG 1.1.1 / Req 14.5).\n * - `audio` / `video`: rendered with native controls; `alt` is an optional\n * accessible label; `captionsUrl` points at a WebVTT `<track>`. `url` must\n * be a direct media file (NOT a YouTube/Vimeo page — use `embed` for those).\n * - `embed`: rendered as a sandboxed `<iframe>` for provider players\n * (YouTube/Vimeo/etc.). `url` MUST be the provider's *embeddable* URL\n * (e.g. `https://www.youtube.com/embed/<id>`). `alt` is REQUIRED and used\n * as the iframe's accessible `title` (WCAG 4.1.2 / 2.4.1).\n */\nexport const MediaSchema = z\n .object({\n type: z.enum(['image', 'audio', 'video', 'embed']),\n url: z.url(),\n alt: z.string().min(1).optional(),\n captionsUrl: z.url().optional(),\n })\n .refine(\n (m) =>\n (m.type !== 'image' && m.type !== 'embed') || (typeof m.alt === 'string' && m.alt.length > 0),\n {\n error: 'image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).',\n path: ['alt'],\n },\n );\n","import { z } from 'zod/v4';\nimport { FeedbackSchema } from './feedback.js';\nimport { MediaSchema } from './media.js';\n\n/** Zod schema for a single Multiple Choice option. */\nexport const MultipleChoiceOptionSchema = z.object({\n id: z.string().min(1),\n text: z.string().min(1),\n isCorrect: z.boolean(),\n feedback: z.string().optional(),\n});\n\n/**\n * Zod schema validating the full Multiple Choice activity data contract.\n *\n * Two semantic guards: (1) at least one option must be correct, otherwise the\n * activity can never be answered correctly; (2) `mode: 'single'` must have\n * exactly one correct option — multiple correct options under single-select\n * make `showCorrectAnswers` and the xAPI correct-response ambiguous and mask\n * authoring errors. Both are unrepresentable in JSON Schema and are dropped\n * from `toJSONSchema` output by design.\n */\nexport const MultipleChoiceDataSchema = z\n .object({\n schemaVersion: z.literal('1.0'),\n type: z.literal('multiple-choice'),\n id: z.string().min(1),\n title: z.string().min(1),\n question: z.string().min(1),\n mode: z.enum(['single', 'multi']),\n options: z.array(MultipleChoiceOptionSchema).min(2).max(10),\n scoringStrategy: z.enum(['all-or-nothing', 'partial']),\n media: MediaSchema.optional(),\n feedback: FeedbackSchema.optional(),\n passThreshold: z.number().min(0).max(1).optional(),\n shuffle: z.boolean().optional(),\n locale: z.string().optional(),\n learningObjectives: z.array(z.string()).optional(),\n difficultyLevel: z.literal([1, 2, 3, 4, 5]).optional(),\n })\n .refine((data) => data.options.some((option) => option.isCorrect), {\n error: 'At least one option must be marked correct.',\n path: ['options'],\n })\n .refine(\n (data) =>\n data.mode !== 'single' || data.options.filter((option) => option.isCorrect).length === 1,\n {\n error: 'Single-select activities (mode: \"single\") must have exactly one correct option.',\n path: ['options'],\n },\n );\n","import { z } from 'zod/v4';\nimport { FillInTheBlanksDataSchema } from './fill-in-the-blanks.js';\nimport { MultipleChoiceDataSchema } from './multiple-choice.js';\n\n/**\n * JSON Schema (Draft 7) representation of the Multiple Choice activity data\n * contract, generated natively by Zod 4. Draft 7 is mandated by Requirement\n * 2.2 for the widest AI-prompt / OpenAPI tooling compatibility. The semantic\n * `.refine()` guards are not representable in JSON Schema and are\n * intentionally omitted — the export captures the *structural* contract only.\n */\nexport const multipleChoiceJsonSchema = z.toJSONSchema(MultipleChoiceDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * JSON Schema (Draft 7) representation of the Fill-in-the-Blanks activity data\n * contract, generated natively by Zod 4. Structural contract only (semantic\n * `.refine()` guards are Zod-only and not representable in JSON Schema).\n */\nexport const fillInTheBlanksJsonSchema = z.toJSONSchema(FillInTheBlanksDataSchema, {\n target: 'draft-7',\n});\n","import type { ActivityDataMap, ActivityType, ValidationResult } from '../types/activity.js';\nimport { FillInTheBlanksDataSchema } from './fill-in-the-blanks.js';\nimport { MultipleChoiceDataSchema } from './multiple-choice.js';\n\nexport { FeedbackSchema } from './feedback.js';\nexport { BlankConfigSchema, FillInTheBlanksDataSchema } from './fill-in-the-blanks.js';\nexport { fillInTheBlanksJsonSchema, multipleChoiceJsonSchema } from './json-schema.js';\nexport { MediaSchema } from './media.js';\nexport { MultipleChoiceDataSchema, MultipleChoiceOptionSchema } from './multiple-choice.js';\n\n/** Maps each activity type to the Zod schema that validates its data. */\nconst schemaMap = {\n 'multiple-choice': MultipleChoiceDataSchema,\n 'fill-in-the-blanks': FillInTheBlanksDataSchema,\n} as const;\n\n/**\n * Validates raw activity data against the schema for the given activity type.\n *\n * @returns `{ success: true, data }` with the typed, validated data, or\n * `{ success: false, errors }` with one entry per failed constraint.\n */\nexport function validateActivity<T extends ActivityType>(\n type: T,\n data: unknown,\n): ValidationResult<ActivityDataMap[T]> {\n const result = schemaMap[type].safeParse(data);\n\n if (result.success) {\n return { success: true, data: result.data as ActivityDataMap[T] };\n }\n\n return {\n success: false,\n errors: result.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n };\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z"]}
|
package/dist/chunk-3JMLUUFS.cjs
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
-
|
|
3
|
-
var _chunkDV466SBJcjs = require('./chunk-DV466SBJ.cjs');
|
|
4
|
-
|
|
5
|
-
// src/scoring/strategies/all-or-nothing.ts
|
|
6
|
-
function allOrNothingStrategy(correctItems) {
|
|
7
|
-
return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// src/scoring/strategies/partial.ts
|
|
11
|
-
function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
|
|
12
|
-
const reward = correctSelected / totalCorrect;
|
|
13
|
-
const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
|
|
14
|
-
return Math.max(0, reward - penalty);
|
|
15
|
-
}
|
|
16
|
-
function partialBlankStrategy(correctBlanks, totalBlanks) {
|
|
17
|
-
return correctBlanks / totalBlanks;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// src/scoring/activity-scorers/fill-in-the-blanks.ts
|
|
21
|
-
function normalize(value, blank) {
|
|
22
|
-
let result = value;
|
|
23
|
-
if (blank.trimWhitespace !== false) {
|
|
24
|
-
result = result.trim();
|
|
25
|
-
}
|
|
26
|
-
if (blank.caseSensitive !== true) {
|
|
27
|
-
result = result.toLowerCase();
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
function scoreFillInTheBlanks(data, response) {
|
|
32
|
-
const details = [];
|
|
33
|
-
const perBlankCorrect = [];
|
|
34
|
-
for (const blank of data.blanks) {
|
|
35
|
-
const rawInput = response.answers[blank.id];
|
|
36
|
-
const input = typeof rawInput === "string" ? rawInput : "";
|
|
37
|
-
const normalizedInput = normalize(input, blank);
|
|
38
|
-
const matched = blank.acceptedAnswers.some(
|
|
39
|
-
(accepted) => normalize(accepted, blank) === normalizedInput
|
|
40
|
-
);
|
|
41
|
-
perBlankCorrect.push(matched);
|
|
42
|
-
details.push({
|
|
43
|
-
itemId: blank.id,
|
|
44
|
-
correct: matched,
|
|
45
|
-
learnerResponse: [input],
|
|
46
|
-
correctResponse: [...blank.acceptedAnswers]
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
const correctBlanks = perBlankCorrect.filter(Boolean).length;
|
|
50
|
-
const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
|
|
51
|
-
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/scoring/activity-scorers/multiple-choice.ts
|
|
55
|
-
function scoreMultipleChoice(data, response) {
|
|
56
|
-
const optionById = new Map(data.options.map((option) => [option.id, option]));
|
|
57
|
-
const selected = new Set(response.selectedOptionIds);
|
|
58
|
-
const totalCorrect = data.options.filter((option) => option.isCorrect).length;
|
|
59
|
-
const totalIncorrect = data.options.length - totalCorrect;
|
|
60
|
-
let scoreValue;
|
|
61
|
-
if (data.scoringStrategy === "all-or-nothing") {
|
|
62
|
-
if (data.mode === "single") {
|
|
63
|
-
scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _ => _.get, 'call', _2 => _2(response.selectedOptionIds[0]), 'optionalAccess', _3 => _3.isCorrect]) === true ? 1 : 0;
|
|
64
|
-
} else {
|
|
65
|
-
const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
|
|
66
|
-
const allCorrectSelected = correctIds.every((id) => selected.has(id));
|
|
67
|
-
scoreValue = selected.size === correctIds.length && allCorrectSelected ? 1 : 0;
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
let correctSelected = 0;
|
|
71
|
-
let incorrectSelected = 0;
|
|
72
|
-
for (const id of selected) {
|
|
73
|
-
const option = optionById.get(id);
|
|
74
|
-
if (_optionalChain([option, 'optionalAccess', _4 => _4.isCorrect])) {
|
|
75
|
-
correctSelected += 1;
|
|
76
|
-
} else {
|
|
77
|
-
incorrectSelected += 1;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
scoreValue = partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect);
|
|
81
|
-
}
|
|
82
|
-
const details = data.options.map((option) => {
|
|
83
|
-
const wasSelected = selected.has(option.id);
|
|
84
|
-
return {
|
|
85
|
-
itemId: option.id,
|
|
86
|
-
correct: wasSelected === option.isCorrect,
|
|
87
|
-
learnerResponse: [wasSelected ? "selected" : "not-selected"],
|
|
88
|
-
correctResponse: [option.isCorrect ? "selected" : "not-selected"]
|
|
89
|
-
};
|
|
90
|
-
});
|
|
91
|
-
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/scoring/index.ts
|
|
95
|
-
var DEFAULT_PASS_THRESHOLD = 0.7;
|
|
96
|
-
function computePassThreshold(activityData, score2) {
|
|
97
|
-
return score2 >= (_nullishCoalesce(activityData.passThreshold, () => ( DEFAULT_PASS_THRESHOLD)));
|
|
98
|
-
}
|
|
99
|
-
function score(activityType, activityData, learnerResponse) {
|
|
100
|
-
let result;
|
|
101
|
-
switch (activityType) {
|
|
102
|
-
case "multiple-choice":
|
|
103
|
-
result = scoreMultipleChoice(
|
|
104
|
-
activityData,
|
|
105
|
-
learnerResponse
|
|
106
|
-
);
|
|
107
|
-
break;
|
|
108
|
-
case "fill-in-the-blanks":
|
|
109
|
-
result = scoreFillInTheBlanks(
|
|
110
|
-
activityData,
|
|
111
|
-
learnerResponse
|
|
112
|
-
);
|
|
113
|
-
break;
|
|
114
|
-
default:
|
|
115
|
-
throw new (0, _chunkDV466SBJcjs.UnknownActivityTypeError)(String(activityType));
|
|
116
|
-
}
|
|
117
|
-
return { ...result, passed: computePassThreshold(activityData, result.score) };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
exports.computePassThreshold = computePassThreshold; exports.score = score;
|
|
124
|
-
//# sourceMappingURL=chunk-3JMLUUFS.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-3JMLUUFS.cjs","../src/scoring/strategies/all-or-nothing.ts","../src/scoring/strategies/partial.ts","../src/scoring/activity-scorers/fill-in-the-blanks.ts","../src/scoring/activity-scorers/multiple-choice.ts","../src/scoring/index.ts"],"names":["score"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-3JMLUUFS.cjs","sourcesContent":[null,"/**\n * All-or-nothing scoring: full credit only when every item is correct.\n *\n * @param correctItems - per-item correctness flags for the relevant items\n * @returns `1` if every item is correct (or the list is empty), otherwise `0`\n */\nexport function allOrNothingStrategy(correctItems: boolean[]): number {\n return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;\n}\n","/**\n * Partial-credit scoring for Multiple Choice — balanced/symmetric scheme.\n *\n * Reward and penalty are each normalised by their own pool: the fraction of\n * correct options found, minus the fraction of distractors wrongly chosen.\n * This is fairer than normalising the penalty by the correct-option count —\n * under that older form a single wrong pick could zero an otherwise-correct\n * response when there was only one correct answer.\n *\n * Result is always in [0, 1]: `reward ∈ [0,1]` and `penalty ∈ [0,1]`, so\n * `reward - penalty ∈ [-1,1]` and `max(0, …)` floors it at 0. Selecting every\n * option yields `1 - 1 = 0`; selecting exactly the correct set yields `1`.\n *\n * @param correctSelected - number of selected options that are correct\n * @param incorrectSelected - number of selected options that are incorrect\n * @param totalCorrect - total correct options; the `MultipleChoiceDataSchema`\n * \"≥1 correct\" guard guarantees this is ≥ 1, so the reward term cannot\n * divide by zero for schema-validated data\n * @param totalIncorrect - total incorrect options (distractors); when `0`\n * (every option is correct) the penalty term is defined as `0`\n */\nexport function partialStrategy(\n correctSelected: number,\n incorrectSelected: number,\n totalCorrect: number,\n totalIncorrect: number,\n): number {\n const reward = correctSelected / totalCorrect;\n const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;\n return Math.max(0, reward - penalty);\n}\n\n/**\n * Partial-credit scoring for Fill-in-the-Blanks: fraction of blanks answered\n * correctly. Each blank is independently right or wrong, so a plain proportion\n * is already fair — no penalty term applies.\n *\n * @param correctBlanks - number of blanks answered correctly\n * @param totalBlanks - total number of blanks; the `FillInTheBlanksDataSchema`\n * `blanks.min(1)` constraint guarantees this is ≥ 1, so division by zero\n * cannot occur for schema-validated data\n */\nexport function partialBlankStrategy(correctBlanks: number, totalBlanks: number): number {\n return correctBlanks / totalBlanks;\n}\n","import type {\n BlankConfig,\n FillInTheBlanksData,\n FillInTheBlanksLearnerResponse,\n ScoringDetail,\n} from '../../types/activity.js';\nimport { allOrNothingStrategy } from '../strategies/all-or-nothing.js';\nimport { partialBlankStrategy } from '../strategies/partial.js';\nimport type { PartialScoringResult } from './multiple-choice.js';\n\n/**\n * Normalises a value for comparison per the blank's rules.\n * Defaults: `trimWhitespace` true, `caseSensitive` false (case-insensitive\n * unless the author explicitly opts in).\n */\nfunction normalize(value: string, blank: BlankConfig): string {\n let result = value;\n if (blank.trimWhitespace !== false) {\n result = result.trim();\n }\n if (blank.caseSensitive !== true) {\n result = result.toLowerCase();\n }\n return result;\n}\n\n/**\n * Scores a Fill-in-the-Blanks response. Each blank is evaluated independently;\n * a missing answer key is treated as empty input and scored incorrect.\n */\nexport function scoreFillInTheBlanks(\n data: FillInTheBlanksData,\n response: FillInTheBlanksLearnerResponse,\n): PartialScoringResult {\n const details: ScoringDetail[] = [];\n const perBlankCorrect: boolean[] = [];\n\n for (const blank of data.blanks) {\n const rawInput = response.answers[blank.id];\n const input = typeof rawInput === 'string' ? rawInput : '';\n const normalizedInput = normalize(input, blank);\n const matched = blank.acceptedAnswers.some(\n (accepted) => normalize(accepted, blank) === normalizedInput,\n );\n\n perBlankCorrect.push(matched);\n details.push({\n itemId: blank.id,\n correct: matched,\n learnerResponse: [input],\n correctResponse: [...blank.acceptedAnswers],\n });\n }\n\n const correctBlanks = perBlankCorrect.filter(Boolean).length;\n const scoreValue =\n data.scoringStrategy === 'all-or-nothing'\n ? allOrNothingStrategy(perBlankCorrect)\n : partialBlankStrategy(correctBlanks, data.blanks.length);\n\n return { score: scoreValue, maxScore: 1, feedback: null, details };\n}\n","import type {\n MultipleChoiceData,\n MultipleChoiceLearnerResponse,\n ScoringDetail,\n ScoringResult,\n} from '../../types/activity.js';\nimport { partialStrategy } from '../strategies/partial.js';\n\n/** ScoringResult without `passed` — the public `score()` fills that in. */\nexport type PartialScoringResult = Omit<ScoringResult, 'passed'>;\n\n/**\n * Scores a Multiple Choice response. Pure; trusts its typed inputs (validation\n * is the component boundary's job). Options are looked up by id; an unknown\n * selected id is treated as an incorrect selection.\n */\nexport function scoreMultipleChoice(\n data: MultipleChoiceData,\n response: MultipleChoiceLearnerResponse,\n): PartialScoringResult {\n const optionById = new Map(data.options.map((option) => [option.id, option]));\n const selected = new Set(response.selectedOptionIds);\n\n const totalCorrect = data.options.filter((option) => option.isCorrect).length;\n const totalIncorrect = data.options.length - totalCorrect;\n\n let scoreValue: number;\n\n if (data.scoringStrategy === 'all-or-nothing') {\n if (data.mode === 'single') {\n scoreValue =\n response.selectedOptionIds.length === 1 &&\n optionById.get(response.selectedOptionIds[0])?.isCorrect === true\n ? 1\n : 0;\n } else {\n const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);\n const allCorrectSelected = correctIds.every((id) => selected.has(id));\n scoreValue = selected.size === correctIds.length && allCorrectSelected ? 1 : 0;\n }\n } else {\n let correctSelected = 0;\n let incorrectSelected = 0;\n for (const id of selected) {\n const option = optionById.get(id);\n if (option?.isCorrect) {\n correctSelected += 1;\n } else {\n incorrectSelected += 1;\n }\n }\n scoreValue = partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect);\n }\n\n const details: ScoringDetail[] = data.options.map((option) => {\n const wasSelected = selected.has(option.id);\n return {\n itemId: option.id,\n correct: wasSelected === option.isCorrect,\n learnerResponse: [wasSelected ? 'selected' : 'not-selected'],\n correctResponse: [option.isCorrect ? 'selected' : 'not-selected'],\n };\n });\n\n return { score: scoreValue, maxScore: 1, feedback: null, details };\n}\n","import { UnknownActivityTypeError } from '../errors.js';\nimport type {\n ActivityData,\n ActivityType,\n FillInTheBlanksData,\n FillInTheBlanksLearnerResponse,\n LearnerResponse,\n MultipleChoiceData,\n MultipleChoiceLearnerResponse,\n ScoringResult,\n} from '../types/activity.js';\nimport { scoreFillInTheBlanks } from './activity-scorers/fill-in-the-blanks.js';\nimport { scoreMultipleChoice } from './activity-scorers/multiple-choice.js';\n\n/** Default minimum scaled score required to pass when `passThreshold` is absent. */\nconst DEFAULT_PASS_THRESHOLD = 0.7;\n\n/**\n * Returns `true` iff `score` meets or exceeds the activity's `passThreshold`,\n * defaulting to {@link DEFAULT_PASS_THRESHOLD} when the field is absent.\n */\nexport function computePassThreshold(activityData: ActivityData, score: number): boolean {\n return score >= (activityData.passThreshold ?? DEFAULT_PASS_THRESHOLD);\n}\n\n/**\n * Scores a learner response against activity data and returns a full\n * {@link ScoringResult}.\n *\n * Pure and deterministic with no side effects. It does **not** re-validate\n * `activityData` — schema validation is the component boundary's\n * responsibility; this is a low-level scoring primitive that trusts its typed\n * inputs. Dispatches on `activityType`; an unrecognised type throws\n * {@link UnknownActivityTypeError}.\n */\nexport function score(\n activityType: ActivityType,\n activityData: ActivityData,\n learnerResponse: LearnerResponse,\n): ScoringResult {\n let result: Omit<ScoringResult, 'passed'>;\n\n switch (activityType) {\n case 'multiple-choice':\n result = scoreMultipleChoice(\n activityData as MultipleChoiceData,\n learnerResponse as MultipleChoiceLearnerResponse,\n );\n break;\n case 'fill-in-the-blanks':\n result = scoreFillInTheBlanks(\n activityData as FillInTheBlanksData,\n learnerResponse as FillInTheBlanksLearnerResponse,\n );\n break;\n default:\n throw new UnknownActivityTypeError(String(activityType));\n }\n\n return { ...result, passed: computePassThreshold(activityData, result.score) };\n}\n"]}
|