@intellectif/lk-core 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +310 -29
- package/dist/schemas.d.ts +310 -29
- 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
|
@@ -0,0 +1,576 @@
|
|
|
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; }// src/count-words.ts
|
|
2
|
+
function countWords(text) {
|
|
3
|
+
if (typeof text !== "string") {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
if (trimmed === "") {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
return trimmed.split(/\s+/).length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/schemas/feedback.ts
|
|
14
|
+
var _v4 = require('zod/v4');
|
|
15
|
+
var FeedbackSchema = _v4.z.looseObject({
|
|
16
|
+
correct: _v4.z.string().min(1).optional(),
|
|
17
|
+
incorrect: _v4.z.string().min(1).optional()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/schemas/media.ts
|
|
21
|
+
|
|
22
|
+
var MediaUrlSchema = _v4.z.union([
|
|
23
|
+
_v4.z.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
24
|
+
error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
|
|
25
|
+
}),
|
|
26
|
+
_v4.z.string().regex(/^\/(?!\/)\S*$/, {
|
|
27
|
+
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
28
|
+
})
|
|
29
|
+
]);
|
|
30
|
+
var MediaSchema = _v4.z.looseObject({
|
|
31
|
+
type: _v4.z.enum(["image", "audio", "video", "embed"]),
|
|
32
|
+
url: MediaUrlSchema,
|
|
33
|
+
alt: _v4.z.string().min(1).optional(),
|
|
34
|
+
captionsUrl: MediaUrlSchema.optional()
|
|
35
|
+
}).refine(
|
|
36
|
+
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
37
|
+
{
|
|
38
|
+
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
39
|
+
path: ["alt"]
|
|
40
|
+
}
|
|
41
|
+
).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
|
|
42
|
+
error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
|
|
43
|
+
path: ["url"]
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/schemas/fill-in-the-blanks.ts
|
|
47
|
+
|
|
48
|
+
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
49
|
+
var TextMatchPolicySchema = _v4.z.looseObject({
|
|
50
|
+
caseSensitive: _v4.z.boolean().optional(),
|
|
51
|
+
trim: _v4.z.boolean().optional(),
|
|
52
|
+
normalize: _v4.z.enum(["none", "NFC", "NFKC"]).optional(),
|
|
53
|
+
foldDiacritics: _v4.z.boolean().optional(),
|
|
54
|
+
collapseInnerWhitespace: _v4.z.boolean().optional(),
|
|
55
|
+
ignorePunctuation: _v4.z.boolean().optional(),
|
|
56
|
+
levenshtein: _v4.z.number().int().min(0).optional(),
|
|
57
|
+
locale: _v4.z.string().optional()
|
|
58
|
+
});
|
|
59
|
+
var BlankConfigSchema = _v4.z.looseObject({
|
|
60
|
+
id: _v4.z.string().min(1),
|
|
61
|
+
acceptedAnswers: _v4.z.array(
|
|
62
|
+
_v4.z.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
63
|
+
error: "Accepted answers must contain non-whitespace characters."
|
|
64
|
+
})
|
|
65
|
+
).min(1),
|
|
66
|
+
caseSensitive: _v4.z.boolean().optional(),
|
|
67
|
+
trimWhitespace: _v4.z.boolean().optional(),
|
|
68
|
+
match: TextMatchPolicySchema.optional(),
|
|
69
|
+
hint: _v4.z.string().optional(),
|
|
70
|
+
feedback: _v4.z.string().optional()
|
|
71
|
+
});
|
|
72
|
+
var FillInTheBlanksDataSchema = _v4.z.looseObject({
|
|
73
|
+
schemaVersion: _v4.z.literal("1.0"),
|
|
74
|
+
type: _v4.z.literal("fill-in-the-blanks"),
|
|
75
|
+
id: _v4.z.string().min(1),
|
|
76
|
+
title: _v4.z.string().min(1),
|
|
77
|
+
passage: _v4.z.string().min(1),
|
|
78
|
+
passageHtml: _v4.z.string().optional(),
|
|
79
|
+
blanks: _v4.z.array(BlankConfigSchema).min(1),
|
|
80
|
+
scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
|
|
81
|
+
media: MediaSchema.optional(),
|
|
82
|
+
feedback: FeedbackSchema.optional(),
|
|
83
|
+
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
84
|
+
locale: _v4.z.string().optional(),
|
|
85
|
+
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
86
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
87
|
+
}).refine(
|
|
88
|
+
(data) => {
|
|
89
|
+
const placeholderCounts = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
91
|
+
const id = match[1];
|
|
92
|
+
placeholderCounts.set(id, (_nullishCoalesce(placeholderCounts.get(id), () => ( 0))) + 1);
|
|
93
|
+
}
|
|
94
|
+
const blankIds = data.blanks.map((blank) => blank.id);
|
|
95
|
+
if (new Set(blankIds).size !== blankIds.length) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (placeholderCounts.size !== blankIds.length) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return blankIds.every((id) => placeholderCounts.get(id) === 1);
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
105
|
+
path: ["passage"]
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// src/schemas/multiple-choice.ts
|
|
110
|
+
|
|
111
|
+
var MultipleChoiceOptionSchema = _v4.z.looseObject({
|
|
112
|
+
id: _v4.z.string().min(1),
|
|
113
|
+
text: _v4.z.string().min(1),
|
|
114
|
+
isCorrect: _v4.z.boolean(),
|
|
115
|
+
feedback: _v4.z.string().optional()
|
|
116
|
+
});
|
|
117
|
+
var MultipleChoiceDataSchema = _v4.z.looseObject({
|
|
118
|
+
schemaVersion: _v4.z.literal("1.0"),
|
|
119
|
+
type: _v4.z.literal("multiple-choice"),
|
|
120
|
+
id: _v4.z.string().min(1),
|
|
121
|
+
title: _v4.z.string().min(1),
|
|
122
|
+
question: _v4.z.string().min(1),
|
|
123
|
+
questionHtml: _v4.z.string().optional(),
|
|
124
|
+
mode: _v4.z.enum(["single", "multi"]),
|
|
125
|
+
options: _v4.z.array(MultipleChoiceOptionSchema).min(2).max(26),
|
|
126
|
+
scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
|
|
127
|
+
media: MediaSchema.optional(),
|
|
128
|
+
feedback: FeedbackSchema.optional(),
|
|
129
|
+
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
130
|
+
shuffle: _v4.z.boolean().optional(),
|
|
131
|
+
locale: _v4.z.string().optional(),
|
|
132
|
+
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
133
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
134
|
+
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
135
|
+
error: "At least one option must be marked correct.",
|
|
136
|
+
path: ["options"]
|
|
137
|
+
}).refine(
|
|
138
|
+
(data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
|
|
139
|
+
{
|
|
140
|
+
error: 'Single-select activities (mode: "single") must have exactly one correct option.',
|
|
141
|
+
path: ["options"]
|
|
142
|
+
}
|
|
143
|
+
).refine((data) => new Set(data.options.map((option) => option.id)).size === data.options.length, {
|
|
144
|
+
error: "Option ids must be unique within the activity.",
|
|
145
|
+
path: ["options"]
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// src/schemas/written-response.ts
|
|
149
|
+
|
|
150
|
+
var WrittenResponseRubricCriterionSchema = _v4.z.looseObject({
|
|
151
|
+
name: _v4.z.string().min(1),
|
|
152
|
+
description: _v4.z.string().optional(),
|
|
153
|
+
weight: _v4.z.number().min(0)
|
|
154
|
+
});
|
|
155
|
+
var WrittenResponseRubricSchema = _v4.z.looseObject({
|
|
156
|
+
label: _v4.z.string().optional(),
|
|
157
|
+
criteria: _v4.z.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
158
|
+
});
|
|
159
|
+
var WrittenResponseDataSchema = _v4.z.looseObject({
|
|
160
|
+
schemaVersion: _v4.z.literal("1.0"),
|
|
161
|
+
type: _v4.z.literal("written-response"),
|
|
162
|
+
id: _v4.z.string().min(1),
|
|
163
|
+
title: _v4.z.string().min(1),
|
|
164
|
+
prompt: _v4.z.string(),
|
|
165
|
+
promptHtml: _v4.z.string().optional(),
|
|
166
|
+
minWords: _v4.z.number().int().min(0),
|
|
167
|
+
maxWords: _v4.z.number().int().min(1),
|
|
168
|
+
rubric: WrittenResponseRubricSchema.optional(),
|
|
169
|
+
languageTarget: _v4.z.string().optional(),
|
|
170
|
+
media: MediaSchema.optional(),
|
|
171
|
+
feedback: FeedbackSchema.optional(),
|
|
172
|
+
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
173
|
+
locale: _v4.z.string().optional(),
|
|
174
|
+
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
175
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
176
|
+
}).refine((data) => data.maxWords >= data.minWords, {
|
|
177
|
+
error: "maxWords must be greater than or equal to minWords.",
|
|
178
|
+
path: ["maxWords"]
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// src/schemas/redacted.ts
|
|
182
|
+
|
|
183
|
+
var redactedBase = {
|
|
184
|
+
/** Marker distinguishing a redacted projection from full activity data. */
|
|
185
|
+
redacted: _v4.z.literal(true),
|
|
186
|
+
schemaVersion: _v4.z.literal("1.0"),
|
|
187
|
+
id: _v4.z.string().min(1),
|
|
188
|
+
title: _v4.z.string().min(1),
|
|
189
|
+
media: MediaSchema.optional(),
|
|
190
|
+
passThreshold: _v4.z.number().min(0).max(1).optional(),
|
|
191
|
+
locale: _v4.z.string().optional(),
|
|
192
|
+
learningObjectives: _v4.z.array(_v4.z.string()).optional(),
|
|
193
|
+
difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
|
|
194
|
+
};
|
|
195
|
+
var RedactedMultipleChoiceOptionSchema = _v4.z.strictObject({
|
|
196
|
+
id: _v4.z.string().min(1),
|
|
197
|
+
text: _v4.z.string().min(1)
|
|
198
|
+
});
|
|
199
|
+
var RedactedMultipleChoiceDataSchema = _v4.z.strictObject({
|
|
200
|
+
...redactedBase,
|
|
201
|
+
type: _v4.z.literal("multiple-choice"),
|
|
202
|
+
question: _v4.z.string().min(1),
|
|
203
|
+
questionHtml: _v4.z.string().optional(),
|
|
204
|
+
mode: _v4.z.enum(["single", "multi"]),
|
|
205
|
+
options: _v4.z.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
|
|
206
|
+
shuffle: _v4.z.boolean().optional()
|
|
207
|
+
});
|
|
208
|
+
var RedactedBlankConfigSchema = _v4.z.strictObject({
|
|
209
|
+
id: _v4.z.string().min(1),
|
|
210
|
+
hint: _v4.z.string().optional()
|
|
211
|
+
});
|
|
212
|
+
var RedactedFillInTheBlanksDataSchema = _v4.z.strictObject({
|
|
213
|
+
...redactedBase,
|
|
214
|
+
type: _v4.z.literal("fill-in-the-blanks"),
|
|
215
|
+
passage: _v4.z.string().min(1),
|
|
216
|
+
passageHtml: _v4.z.string().optional(),
|
|
217
|
+
blanks: _v4.z.array(RedactedBlankConfigSchema).min(1)
|
|
218
|
+
});
|
|
219
|
+
var RedactedWrittenResponseDataSchema = _v4.z.strictObject({
|
|
220
|
+
...redactedBase,
|
|
221
|
+
type: _v4.z.literal("written-response"),
|
|
222
|
+
prompt: _v4.z.string(),
|
|
223
|
+
promptHtml: _v4.z.string().optional(),
|
|
224
|
+
minWords: _v4.z.number().int().min(0),
|
|
225
|
+
maxWords: _v4.z.number().int().min(1),
|
|
226
|
+
rubric: WrittenResponseRubricSchema.optional(),
|
|
227
|
+
languageTarget: _v4.z.string().optional()
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// src/scoring/text-match.ts
|
|
231
|
+
var COMBINING_MARKS_RE = /\p{M}/gu;
|
|
232
|
+
var PUNCTUATION_RE = /\p{P}/gu;
|
|
233
|
+
function applyBaseline(value, policy) {
|
|
234
|
+
let result = value;
|
|
235
|
+
if (policy.trim !== false) {
|
|
236
|
+
result = result.trim();
|
|
237
|
+
}
|
|
238
|
+
if (policy.caseSensitive !== true) {
|
|
239
|
+
result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function applyNormalization(value, policy) {
|
|
244
|
+
let result = value;
|
|
245
|
+
if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
|
|
246
|
+
result = result.normalize(policy.normalize);
|
|
247
|
+
}
|
|
248
|
+
if (policy.ignorePunctuation === true) {
|
|
249
|
+
result = result.replace(PUNCTUATION_RE, "");
|
|
250
|
+
}
|
|
251
|
+
if (policy.collapseInnerWhitespace === true) {
|
|
252
|
+
result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
|
|
253
|
+
if (policy.trim !== false) {
|
|
254
|
+
result = result.trim();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function applyDiacriticFold(value) {
|
|
260
|
+
return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
|
|
261
|
+
}
|
|
262
|
+
function levenshteinDistance(a, b, max) {
|
|
263
|
+
if (a === b) return 0;
|
|
264
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
265
|
+
if (a.length === 0) return b.length;
|
|
266
|
+
if (b.length === 0) return a.length;
|
|
267
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
268
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
269
|
+
const current = [i];
|
|
270
|
+
let rowMin = i;
|
|
271
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
272
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
273
|
+
const value = Math.min(
|
|
274
|
+
previous[j] + 1,
|
|
275
|
+
current[j - 1] + 1,
|
|
276
|
+
previous[j - 1] + cost
|
|
277
|
+
);
|
|
278
|
+
current.push(value);
|
|
279
|
+
if (value < rowMin) rowMin = value;
|
|
280
|
+
}
|
|
281
|
+
if (rowMin > max) return max + 1;
|
|
282
|
+
previous = current;
|
|
283
|
+
}
|
|
284
|
+
return previous[b.length];
|
|
285
|
+
}
|
|
286
|
+
function matchText(input, accepted, policy = {}) {
|
|
287
|
+
const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
|
|
288
|
+
const baselineInput = applyBaseline(input, policy);
|
|
289
|
+
const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
|
|
290
|
+
if (baselineAccepted.some((answer) => answer === baselineInput)) {
|
|
291
|
+
return { matched: true, via: "exact" };
|
|
292
|
+
}
|
|
293
|
+
const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
|
|
294
|
+
const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
|
|
295
|
+
const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
|
|
296
|
+
if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
|
|
297
|
+
return { matched: true, via: "normalized" };
|
|
298
|
+
}
|
|
299
|
+
const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
|
|
300
|
+
const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
|
|
301
|
+
if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
|
|
302
|
+
return { matched: true, via: "folded" };
|
|
303
|
+
}
|
|
304
|
+
const maxDistance = _nullishCoalesce(policy.levenshtein, () => ( 0));
|
|
305
|
+
if (maxDistance > 0 && foldedAccepted.some(
|
|
306
|
+
(answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
|
|
307
|
+
)) {
|
|
308
|
+
return { matched: true, via: "fuzzy" };
|
|
309
|
+
}
|
|
310
|
+
return { matched: false, via: "none" };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/registry/registry.ts
|
|
314
|
+
var registry = /* @__PURE__ */ new Map();
|
|
315
|
+
function defineActivityType(descriptor) {
|
|
316
|
+
return descriptor;
|
|
317
|
+
}
|
|
318
|
+
function registerActivityType(descriptor) {
|
|
319
|
+
const existing = registry.get(descriptor.type);
|
|
320
|
+
if (existing !== void 0) {
|
|
321
|
+
if (existing === descriptor) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Activity type "${descriptor.type}" is already registered. Registering a different descriptor for an existing type is not allowed.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
registry.set(descriptor.type, descriptor);
|
|
329
|
+
}
|
|
330
|
+
function getActivityTypeDescriptor(type) {
|
|
331
|
+
return registry.get(type);
|
|
332
|
+
}
|
|
333
|
+
function registeredActivityTypes() {
|
|
334
|
+
return [...registry.keys()];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/scoring/strategies/all-or-nothing.ts
|
|
338
|
+
function allOrNothingStrategy(correctItems) {
|
|
339
|
+
return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/scoring/strategies/partial.ts
|
|
343
|
+
function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
|
|
344
|
+
const reward = correctSelected / totalCorrect;
|
|
345
|
+
const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
|
|
346
|
+
return Math.max(0, reward - penalty);
|
|
347
|
+
}
|
|
348
|
+
function partialBlankStrategy(correctBlanks, totalBlanks) {
|
|
349
|
+
return correctBlanks / totalBlanks;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/scoring/activity-scorers/fill-in-the-blanks.ts
|
|
353
|
+
function policyFor(blank) {
|
|
354
|
+
return {
|
|
355
|
+
...blank.caseSensitive !== void 0 ? { caseSensitive: blank.caseSensitive } : {},
|
|
356
|
+
...blank.trimWhitespace !== void 0 ? { trim: blank.trimWhitespace } : {},
|
|
357
|
+
...blank.match
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function scoreFillInTheBlanks(data, response) {
|
|
361
|
+
const details = [];
|
|
362
|
+
const perBlankCorrect = [];
|
|
363
|
+
for (const blank of data.blanks) {
|
|
364
|
+
const rawInput = response.answers[blank.id];
|
|
365
|
+
const input = typeof rawInput === "string" ? rawInput : "";
|
|
366
|
+
const matched = matchText(input, blank.acceptedAnswers, policyFor(blank)).matched;
|
|
367
|
+
perBlankCorrect.push(matched);
|
|
368
|
+
details.push({
|
|
369
|
+
itemId: blank.id,
|
|
370
|
+
correct: matched,
|
|
371
|
+
outcome: matched ? "correct" : "incorrect",
|
|
372
|
+
learnerResponse: [input],
|
|
373
|
+
correctResponse: [...blank.acceptedAnswers],
|
|
374
|
+
weight: 1
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const correctBlanks = perBlankCorrect.filter(Boolean).length;
|
|
378
|
+
const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
|
|
379
|
+
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/scoring/activity-scorers/multiple-choice.ts
|
|
383
|
+
function scoreMultipleChoice(data, response) {
|
|
384
|
+
const optionById = new Map(data.options.map((option) => [option.id, option]));
|
|
385
|
+
const selected = new Set(response.selectedOptionIds);
|
|
386
|
+
const totalCorrect = data.options.filter((option) => option.isCorrect).length;
|
|
387
|
+
const totalIncorrect = data.options.length - totalCorrect;
|
|
388
|
+
let scoreValue;
|
|
389
|
+
if (data.scoringStrategy === "all-or-nothing") {
|
|
390
|
+
if (data.mode === "single") {
|
|
391
|
+
scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _2 => _2.get, 'call', _3 => _3(response.selectedOptionIds[0]), 'optionalAccess', _4 => _4.isCorrect]) === true ? 1 : 0;
|
|
392
|
+
} else {
|
|
393
|
+
const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
|
|
394
|
+
const allCorrectSelected = correctIds.every((id) => selected.has(id));
|
|
395
|
+
scoreValue = selected.size === correctIds.length && allCorrectSelected ? 1 : 0;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
let correctSelected = 0;
|
|
399
|
+
let incorrectSelected = 0;
|
|
400
|
+
for (const id of selected) {
|
|
401
|
+
const option = optionById.get(id);
|
|
402
|
+
if (_optionalChain([option, 'optionalAccess', _5 => _5.isCorrect])) {
|
|
403
|
+
correctSelected += 1;
|
|
404
|
+
} else {
|
|
405
|
+
incorrectSelected += 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
scoreValue = partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect);
|
|
409
|
+
}
|
|
410
|
+
const details = data.options.map((option) => {
|
|
411
|
+
const wasSelected = selected.has(option.id);
|
|
412
|
+
const outcome = wasSelected ? option.isCorrect ? "correct" : "incorrect" : option.isCorrect ? "incorrect-omission" : "correct-omission";
|
|
413
|
+
return {
|
|
414
|
+
itemId: option.id,
|
|
415
|
+
correct: wasSelected === option.isCorrect,
|
|
416
|
+
outcome,
|
|
417
|
+
learnerResponse: [wasSelected ? "selected" : "not-selected"],
|
|
418
|
+
correctResponse: [option.isCorrect ? "selected" : "not-selected"],
|
|
419
|
+
weight: 1
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/registry/builtins.ts
|
|
426
|
+
var SHARED_PUBLIC_FIELDS = {
|
|
427
|
+
schemaVersion: "public",
|
|
428
|
+
type: "public",
|
|
429
|
+
id: "public",
|
|
430
|
+
title: "public",
|
|
431
|
+
media: "public",
|
|
432
|
+
passThreshold: "public",
|
|
433
|
+
locale: "public",
|
|
434
|
+
learningObjectives: "public",
|
|
435
|
+
difficultyLevel: "public"
|
|
436
|
+
};
|
|
437
|
+
var MULTIPLE_CHOICE_FIELD_POLICY = {
|
|
438
|
+
...SHARED_PUBLIC_FIELDS,
|
|
439
|
+
question: "public",
|
|
440
|
+
questionHtml: "public",
|
|
441
|
+
mode: "public",
|
|
442
|
+
shuffle: "public",
|
|
443
|
+
scoringStrategy: "answer-key",
|
|
444
|
+
feedback: "answer-key",
|
|
445
|
+
options: {
|
|
446
|
+
id: "public",
|
|
447
|
+
text: "public",
|
|
448
|
+
isCorrect: "answer-key",
|
|
449
|
+
feedback: "answer-key"
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
var FILL_IN_THE_BLANKS_FIELD_POLICY = {
|
|
453
|
+
...SHARED_PUBLIC_FIELDS,
|
|
454
|
+
passage: "public",
|
|
455
|
+
passageHtml: "public",
|
|
456
|
+
scoringStrategy: "answer-key",
|
|
457
|
+
feedback: "answer-key",
|
|
458
|
+
blanks: {
|
|
459
|
+
id: "public",
|
|
460
|
+
hint: "public",
|
|
461
|
+
acceptedAnswers: "answer-key",
|
|
462
|
+
caseSensitive: "answer-key",
|
|
463
|
+
trimWhitespace: "answer-key",
|
|
464
|
+
match: "answer-key",
|
|
465
|
+
feedback: "answer-key"
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var WRITTEN_RESPONSE_FIELD_POLICY = {
|
|
469
|
+
...SHARED_PUBLIC_FIELDS,
|
|
470
|
+
prompt: "public",
|
|
471
|
+
promptHtml: "public",
|
|
472
|
+
minWords: "public",
|
|
473
|
+
maxWords: "public",
|
|
474
|
+
languageTarget: "public",
|
|
475
|
+
feedback: "answer-key",
|
|
476
|
+
// A rubric is a LEARNER affordance, not a grader secret: it tells the
|
|
477
|
+
// learner what they are being graded on, which is pedagogically the point
|
|
478
|
+
// of publishing one. (Classifying it author-only broke real deployments
|
|
479
|
+
// that render a rubric panel during the attempt.) A deployment that wants
|
|
480
|
+
// it hidden can tighten this per call via `redact(data, { policy })`.
|
|
481
|
+
rubric: "public"
|
|
482
|
+
};
|
|
483
|
+
var multipleChoiceType = defineActivityType({
|
|
484
|
+
type: "multiple-choice",
|
|
485
|
+
// zod4 optional outputs are `T | undefined`; the hand-written wire types use
|
|
486
|
+
// exact optionals. Structurally identical at runtime — cast is type-level only.
|
|
487
|
+
schema: MultipleChoiceDataSchema,
|
|
488
|
+
scoring: { kind: "sync", score: scoreMultipleChoice },
|
|
489
|
+
isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _6 => _6.selectedOptionIds, 'access', _7 => _7.length]), () => ( 0))) > 0,
|
|
490
|
+
fieldPolicy: MULTIPLE_CHOICE_FIELD_POLICY,
|
|
491
|
+
redactedSchema: RedactedMultipleChoiceDataSchema,
|
|
492
|
+
interop: {
|
|
493
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
494
|
+
xapiInteractionType: "choice",
|
|
495
|
+
correctResponsesPattern: (data) => [
|
|
496
|
+
data.options.filter((option) => option.isCorrect).map((option) => option.id).join("[,]")
|
|
497
|
+
]
|
|
498
|
+
},
|
|
499
|
+
interactions: ["option-selected", "option-deselected", "submitted"]
|
|
500
|
+
});
|
|
501
|
+
var fillInTheBlanksType = defineActivityType({
|
|
502
|
+
type: "fill-in-the-blanks",
|
|
503
|
+
schema: FillInTheBlanksDataSchema,
|
|
504
|
+
scoring: { kind: "sync", score: scoreFillInTheBlanks },
|
|
505
|
+
isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _8 => _8.answers]), () => ( {}))).some((answer) => answer.trim().length > 0),
|
|
506
|
+
fieldPolicy: FILL_IN_THE_BLANKS_FIELD_POLICY,
|
|
507
|
+
redactedSchema: RedactedFillInTheBlanksDataSchema,
|
|
508
|
+
interop: {
|
|
509
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
510
|
+
xapiInteractionType: "fill-in",
|
|
511
|
+
// xAPI fill-in pattern: blank answers joined with "[,]". Only the first
|
|
512
|
+
// accepted answer per blank is emitted (full alternates would explode
|
|
513
|
+
// combinatorially); the complete key lives in the activity data.
|
|
514
|
+
correctResponsesPattern: (data) => [
|
|
515
|
+
data.blanks.map((blank) => _nullishCoalesce(blank.acceptedAnswers[0], () => ( ""))).join("[,]")
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
interactions: ["blank-filled", "hint-requested", "submitted"]
|
|
519
|
+
});
|
|
520
|
+
var writtenResponseType = defineActivityType({
|
|
521
|
+
type: "written-response",
|
|
522
|
+
schema: WrittenResponseDataSchema,
|
|
523
|
+
scoring: {
|
|
524
|
+
kind: "deferred",
|
|
525
|
+
reason: "requires_async_grading",
|
|
526
|
+
partial: (data, response) => {
|
|
527
|
+
const wordCount = countWords(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _9 => _9.text]), () => ( "")));
|
|
528
|
+
return {
|
|
529
|
+
withinWordBounds: response !== void 0 && wordCount >= data.minWords && wordCount <= data.maxWords,
|
|
530
|
+
wordCount
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _10 => _10.text, 'access', _11 => _11.trim, 'call', _12 => _12(), 'access', _13 => _13.length]), () => ( 0))) > 0,
|
|
535
|
+
fieldPolicy: WRITTEN_RESPONSE_FIELD_POLICY,
|
|
536
|
+
redactedSchema: RedactedWrittenResponseDataSchema,
|
|
537
|
+
interop: {
|
|
538
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
539
|
+
xapiInteractionType: "long-fill-in",
|
|
540
|
+
correctResponsesPattern: () => []
|
|
541
|
+
},
|
|
542
|
+
interactions: ["text-changed", "submitted"]
|
|
543
|
+
});
|
|
544
|
+
registerActivityType(multipleChoiceType);
|
|
545
|
+
registerActivityType(fillInTheBlanksType);
|
|
546
|
+
registerActivityType(writtenResponseType);
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
exports.countWords = countWords; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.MediaSchema = MediaSchema; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType;
|
|
576
|
+
//# sourceMappingURL=chunk-QOAORTA4.cjs.map
|