@intellectif/lk-core 0.15.0 → 0.17.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/CHANGELOG.md +74 -0
- package/dist/{activity-C7cR8U9n.d.cts → activity-BmjBjXVJ.d.cts} +218 -3
- package/dist/{activity-C7cR8U9n.d.ts → activity-BmjBjXVJ.d.ts} +218 -3
- package/dist/{chunk-W7AQP6AV.cjs → chunk-2CV6NU32.cjs} +24 -22
- package/dist/chunk-2CV6NU32.cjs.map +1 -0
- package/dist/{chunk-DPHX4E7X.js → chunk-D3GP5MET.js} +718 -662
- package/dist/chunk-D3GP5MET.js.map +1 -0
- package/dist/{chunk-QFWEF5ST.cjs → chunk-D4TOPSHV.cjs} +539 -483
- package/dist/chunk-D4TOPSHV.cjs.map +1 -0
- package/dist/{chunk-7ETU6EXQ.cjs → chunk-EOFWSGLU.cjs} +4 -4
- package/dist/{chunk-7ETU6EXQ.cjs.map → chunk-EOFWSGLU.cjs.map} +1 -1
- package/dist/{chunk-L6YBQXSB.js → chunk-WXS5EB65.js} +2 -2
- package/dist/{chunk-54XYQ3CS.js → chunk-X5DAXBJI.js} +2 -2
- package/dist/{chunk-5KWF34LJ.cjs → chunk-XNMKDFPW.cjs} +31 -31
- package/dist/{chunk-5KWF34LJ.cjs.map → chunk-XNMKDFPW.cjs.map} +1 -1
- package/dist/{chunk-EEMVWBPP.js → chunk-Z7QQZ53L.js} +10 -8
- package/dist/chunk-Z7QQZ53L.js.map +1 -0
- package/dist/{index-BWyMDlUS.d.cts → index-CvfeD_cU.d.cts} +144 -86
- package/dist/{index-D10NaztZ.d.ts → index-G_KusUcK.d.ts} +144 -86
- package/dist/index.cjs +544 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -5
- package/dist/index.d.ts +135 -5
- package/dist/index.js +426 -10
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +5 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +4 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-DPHX4E7X.js.map +0 -1
- package/dist/chunk-EEMVWBPP.js.map +0 -1
- package/dist/chunk-QFWEF5ST.cjs.map +0 -1
- package/dist/chunk-W7AQP6AV.cjs.map +0 -1
- /package/dist/{chunk-L6YBQXSB.js.map → chunk-WXS5EB65.js.map} +0 -0
- /package/dist/{chunk-54XYQ3CS.js.map → chunk-X5DAXBJI.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -16,14 +16,14 @@ import {
|
|
|
16
16
|
roundGrade,
|
|
17
17
|
score,
|
|
18
18
|
validateSpeechAssessment
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-WXS5EB65.js";
|
|
20
20
|
import {
|
|
21
21
|
XAPIVerb,
|
|
22
22
|
XAPI_VERB_DISPLAY,
|
|
23
23
|
validateXAPIStatement,
|
|
24
24
|
xAPIBuilder,
|
|
25
25
|
xapiDefinitionFor
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-X5DAXBJI.js";
|
|
27
27
|
import {
|
|
28
28
|
INTERACTIVE_VIDEO_ITEM_TYPES,
|
|
29
29
|
ItemGroupSchema,
|
|
@@ -49,9 +49,10 @@ import {
|
|
|
49
49
|
validateActivity,
|
|
50
50
|
validateItemGroup,
|
|
51
51
|
writtenResponseJsonSchema
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-Z7QQZ53L.js";
|
|
53
53
|
import {
|
|
54
54
|
ActivitySchemaError,
|
|
55
|
+
AiPermissionsSchema,
|
|
55
56
|
BlankConfigSchema,
|
|
56
57
|
DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
|
|
57
58
|
DICTATION_MAX_EQUIVALENCES,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
MultipleChoiceOptionMediaSchema,
|
|
81
82
|
MultipleChoiceOptionSchema,
|
|
82
83
|
NativeControlHintSchema,
|
|
84
|
+
PLACEHOLDER_RE,
|
|
83
85
|
READ_ALOUD_MAX_DIMENSION_WEIGHT,
|
|
84
86
|
READ_ALOUD_MAX_REFERENCE_LENGTH,
|
|
85
87
|
READ_ALOUD_MAX_SECONDS,
|
|
@@ -118,7 +120,7 @@ import {
|
|
|
118
120
|
fillInTheBlanksType,
|
|
119
121
|
gapSelectType,
|
|
120
122
|
getActivityTypeDescriptor,
|
|
121
|
-
|
|
123
|
+
groupCaptionsField,
|
|
122
124
|
isMissingId,
|
|
123
125
|
isRecord,
|
|
124
126
|
isUnset,
|
|
@@ -134,7 +136,406 @@ import {
|
|
|
134
136
|
registeredActivityTypes,
|
|
135
137
|
withValidationScope,
|
|
136
138
|
writtenResponseType
|
|
137
|
-
} from "./chunk-
|
|
139
|
+
} from "./chunk-D3GP5MET.js";
|
|
140
|
+
|
|
141
|
+
// src/ai.ts
|
|
142
|
+
var AI_EXPLANATION_TYPES = [
|
|
143
|
+
"multiple-choice",
|
|
144
|
+
"fill-in-the-blanks",
|
|
145
|
+
"gap-select",
|
|
146
|
+
"dictation"
|
|
147
|
+
];
|
|
148
|
+
var AI_HINT_TYPES = [
|
|
149
|
+
"multiple-choice",
|
|
150
|
+
"fill-in-the-blanks",
|
|
151
|
+
"gap-select"
|
|
152
|
+
];
|
|
153
|
+
var AI_TEXT_MAX_LENGTH = 2e3;
|
|
154
|
+
function aiSupports(activityType, feature) {
|
|
155
|
+
const types = feature === "hint" ? AI_HINT_TYPES : AI_EXPLANATION_TYPES;
|
|
156
|
+
return types.includes(activityType);
|
|
157
|
+
}
|
|
158
|
+
function aiAllowedByContent(data, feature) {
|
|
159
|
+
const permissions = data.ai;
|
|
160
|
+
if (typeof permissions !== "object" || permissions === null) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const key = feature === "hint" ? "hints" : "explanations";
|
|
164
|
+
return permissions[key] !== false;
|
|
165
|
+
}
|
|
166
|
+
function aiGradeOf(result) {
|
|
167
|
+
const ratio = result.maxScore > 0 ? result.score / result.maxScore : 0;
|
|
168
|
+
let category = "partly-correct";
|
|
169
|
+
if (ratio >= 1 - 1e-9) {
|
|
170
|
+
category = "correct";
|
|
171
|
+
} else if (ratio <= 1e-9) {
|
|
172
|
+
category = "incorrect";
|
|
173
|
+
}
|
|
174
|
+
return { score: result.score, maxScore: result.maxScore, passed: result.passed, category };
|
|
175
|
+
}
|
|
176
|
+
function placeholderOrder(passage) {
|
|
177
|
+
return [...passage.matchAll(PLACEHOLDER_RE)].map((match) => match[1]);
|
|
178
|
+
}
|
|
179
|
+
function numberedPassage(passage) {
|
|
180
|
+
let position = 0;
|
|
181
|
+
return passage.replace(PLACEHOLDER_RE, () => {
|
|
182
|
+
position += 1;
|
|
183
|
+
return `[${position}]`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function isAnswerOption(detail, chosen) {
|
|
187
|
+
switch (detail.outcome) {
|
|
188
|
+
case "correct":
|
|
189
|
+
case "incorrect-omission":
|
|
190
|
+
return true;
|
|
191
|
+
case "incorrect":
|
|
192
|
+
case "correct-omission":
|
|
193
|
+
return false;
|
|
194
|
+
default:
|
|
195
|
+
return chosen ? detail.correct : !detail.correct;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
var firstString = (value) => Array.isArray(value) ? value[0] : value;
|
|
199
|
+
var listOf = (value) => value === void 0 ? null : Array.isArray(value) ? [...value] : [value];
|
|
200
|
+
function emptyResponse(type) {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case "multiple-choice":
|
|
203
|
+
return { type: "multiple-choice", selectedOptionIds: [] };
|
|
204
|
+
case "fill-in-the-blanks":
|
|
205
|
+
return { type: "fill-in-the-blanks", answers: {} };
|
|
206
|
+
case "gap-select":
|
|
207
|
+
return { type: "gap-select", selections: {} };
|
|
208
|
+
case "dictation":
|
|
209
|
+
return { type: "dictation", text: "" };
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
var base = (data) => ({
|
|
215
|
+
activityId: data.id,
|
|
216
|
+
title: data.title,
|
|
217
|
+
...typeof data.locale === "string" ? { locale: data.locale } : {}
|
|
218
|
+
});
|
|
219
|
+
function buildAiFacts(data, response, details) {
|
|
220
|
+
const byId = new Map((details ?? []).map((detail) => [detail.itemId, detail]));
|
|
221
|
+
const full = data.redacted !== true;
|
|
222
|
+
switch (data.type) {
|
|
223
|
+
case "multiple-choice": {
|
|
224
|
+
const item = data;
|
|
225
|
+
const chosen = new Set(
|
|
226
|
+
response?.type === "multiple-choice" ? response.selectedOptionIds : []
|
|
227
|
+
);
|
|
228
|
+
return {
|
|
229
|
+
...base(data),
|
|
230
|
+
activityType: "multiple-choice",
|
|
231
|
+
question: item.question,
|
|
232
|
+
mode: item.mode,
|
|
233
|
+
options: item.options.map((option) => {
|
|
234
|
+
const detail = byId.get(option.id);
|
|
235
|
+
const wasChosen = chosen.has(option.id);
|
|
236
|
+
let correct = null;
|
|
237
|
+
if (full && typeof option.isCorrect === "boolean") {
|
|
238
|
+
correct = option.isCorrect;
|
|
239
|
+
} else if (detail !== void 0) {
|
|
240
|
+
correct = isAnswerOption(detail, wasChosen);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
id: option.id,
|
|
244
|
+
text: option.text,
|
|
245
|
+
chosen: wasChosen,
|
|
246
|
+
correct,
|
|
247
|
+
...typeof option.feedback === "string" ? { feedback: option.feedback } : {}
|
|
248
|
+
};
|
|
249
|
+
})
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
case "fill-in-the-blanks": {
|
|
253
|
+
const item = data;
|
|
254
|
+
const answers = response?.type === "fill-in-the-blanks" ? response.answers : {};
|
|
255
|
+
const blanks = new Map(item.blanks.map((blank) => [blank.id, blank]));
|
|
256
|
+
return {
|
|
257
|
+
...base(data),
|
|
258
|
+
activityType: "fill-in-the-blanks",
|
|
259
|
+
passage: numberedPassage(item.passage),
|
|
260
|
+
blanks: placeholderOrder(item.passage).map((id, index) => {
|
|
261
|
+
const blank = blanks.get(id);
|
|
262
|
+
const detail = byId.get(id);
|
|
263
|
+
const accepted = full && Array.isArray(blank?.acceptedAnswers) ? [...blank.acceptedAnswers] : listOf(detail?.correctResponse);
|
|
264
|
+
return {
|
|
265
|
+
id,
|
|
266
|
+
position: index + 1,
|
|
267
|
+
typed: typeof answers[id] === "string" ? answers[id] : "",
|
|
268
|
+
accepted,
|
|
269
|
+
correct: detail === void 0 ? null : detail.correct,
|
|
270
|
+
...typeof blank?.hint === "string" ? { hint: blank.hint } : {},
|
|
271
|
+
...typeof blank?.feedback === "string" ? { feedback: blank.feedback } : {}
|
|
272
|
+
};
|
|
273
|
+
})
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
case "gap-select": {
|
|
277
|
+
const item = data;
|
|
278
|
+
const selections = response?.type === "gap-select" ? response.selections : {};
|
|
279
|
+
const gaps = new Map(item.gaps.map((gap) => [gap.id, gap]));
|
|
280
|
+
const banks = new Map((item.banks ?? []).map((bank) => [bank.id, bank]));
|
|
281
|
+
return {
|
|
282
|
+
...base(data),
|
|
283
|
+
activityType: "gap-select",
|
|
284
|
+
passage: numberedPassage(item.passage),
|
|
285
|
+
gaps: placeholderOrder(item.passage).map((id, index) => {
|
|
286
|
+
const gap = gaps.get(id);
|
|
287
|
+
const choices = gap?.choices ?? (gap?.bankId === void 0 ? [] : banks.get(gap.bankId)?.choices ?? []);
|
|
288
|
+
const textOf = (choiceId) => choiceId === void 0 ? null : choices.find((choice) => choice.id === choiceId)?.text ?? null;
|
|
289
|
+
const detail = byId.get(id);
|
|
290
|
+
const answerId = full && typeof gap?.correctChoiceId === "string" ? gap.correctChoiceId : firstString(detail?.correctResponse);
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
position: index + 1,
|
|
294
|
+
choices: choices.map((choice) => choice.text),
|
|
295
|
+
chosen: textOf(selections[id]),
|
|
296
|
+
answer: textOf(answerId),
|
|
297
|
+
correct: detail === void 0 ? null : detail.correct,
|
|
298
|
+
...typeof gap?.feedback === "string" ? { feedback: gap.feedback } : {}
|
|
299
|
+
};
|
|
300
|
+
})
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
case "dictation": {
|
|
304
|
+
const item = data;
|
|
305
|
+
const typed = response?.type === "dictation" ? response.text : "";
|
|
306
|
+
const hasKey = full && typeof item.transcript === "string";
|
|
307
|
+
let words = null;
|
|
308
|
+
if (hasKey) {
|
|
309
|
+
words = alignDictation(item, typed).words.map((word) => ({
|
|
310
|
+
expected: word.reference,
|
|
311
|
+
typed: word.attempt,
|
|
312
|
+
status: word.status
|
|
313
|
+
}));
|
|
314
|
+
} else if (details !== null && details.length > 0) {
|
|
315
|
+
words = details.map((detail) => {
|
|
316
|
+
const expected = firstString(detail.correctResponse) ?? "";
|
|
317
|
+
const attempt = firstString(detail.learnerResponse) ?? "";
|
|
318
|
+
let status = "incorrect";
|
|
319
|
+
if (detail.correct) {
|
|
320
|
+
status = "correct";
|
|
321
|
+
} else if (attempt === "") {
|
|
322
|
+
status = "missing";
|
|
323
|
+
} else if (expected === "") {
|
|
324
|
+
status = "extra";
|
|
325
|
+
}
|
|
326
|
+
return { expected, typed: attempt, status };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
...base(data),
|
|
331
|
+
activityType: "dictation",
|
|
332
|
+
transcript: hasKey ? item.transcript : null,
|
|
333
|
+
typed,
|
|
334
|
+
words
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function gradeLocally(data, response) {
|
|
342
|
+
if (data.redacted === true) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const result = score(data.type, data, response);
|
|
347
|
+
return { details: result.details, grade: aiGradeOf(result) };
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function aiExplanationRequest(input) {
|
|
353
|
+
const { data, response, outcome, learnerLocale } = input;
|
|
354
|
+
if (!aiSupports(data.type, "explanation") || !aiAllowedByContent(data, "explanation")) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
let details = null;
|
|
358
|
+
let grade = null;
|
|
359
|
+
if (outcome?.status === "scored") {
|
|
360
|
+
details = outcome.details;
|
|
361
|
+
grade = aiGradeOf(outcome);
|
|
362
|
+
} else if (response !== null) {
|
|
363
|
+
const local = gradeLocally(data, response);
|
|
364
|
+
if (local !== null) {
|
|
365
|
+
details = local.details;
|
|
366
|
+
grade = local.grade;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (grade === null) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const facts = buildAiFacts(data, response, details);
|
|
373
|
+
if (facts === null) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
feature: "explanation",
|
|
378
|
+
facts,
|
|
379
|
+
grade,
|
|
380
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function aiHintRequest(input) {
|
|
384
|
+
const { data, previousHints, learnerLocale } = input;
|
|
385
|
+
if (!aiSupports(data.type, "hint") || !aiAllowedByContent(data, "hint") || data.redacted === true) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const response = input.response ?? emptyResponse(data.type);
|
|
389
|
+
const local = response === null ? null : gradeLocally(data, response);
|
|
390
|
+
const facts = buildAiFacts(data, response, local?.details ?? null);
|
|
391
|
+
if (facts === null) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
feature: "hint",
|
|
396
|
+
facts,
|
|
397
|
+
hintNumber: previousHints.length + 1,
|
|
398
|
+
previousHints: [...previousHints],
|
|
399
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
var VERDICTS = ["correct", "partly-correct", "incorrect"];
|
|
403
|
+
function provenanceOf(raw) {
|
|
404
|
+
if (typeof raw !== "object" || raw === null) {
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
407
|
+
const kept = {};
|
|
408
|
+
for (const key of ["model", "promptHash", "generatedAt"]) {
|
|
409
|
+
const value = raw[key];
|
|
410
|
+
if (typeof value === "string" && value.length > 0 && value.length <= 200) {
|
|
411
|
+
kept[key] = value;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return Object.keys(kept).length > 0 ? kept : void 0;
|
|
415
|
+
}
|
|
416
|
+
function cleanText(text) {
|
|
417
|
+
return text.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u2028\u2029]/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
418
|
+
}
|
|
419
|
+
function readAiTextResult(raw) {
|
|
420
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
421
|
+
return { ok: false, refusal: "malformed" };
|
|
422
|
+
}
|
|
423
|
+
const record = raw;
|
|
424
|
+
if (typeof record.text !== "string") {
|
|
425
|
+
return { ok: false, refusal: "malformed" };
|
|
426
|
+
}
|
|
427
|
+
if (record.verdict !== void 0 && !VERDICTS.includes(record.verdict)) {
|
|
428
|
+
return { ok: false, refusal: "malformed" };
|
|
429
|
+
}
|
|
430
|
+
const text = cleanText(record.text);
|
|
431
|
+
if (text === "") {
|
|
432
|
+
return { ok: false, refusal: "empty" };
|
|
433
|
+
}
|
|
434
|
+
if ([...text].length > AI_TEXT_MAX_LENGTH) {
|
|
435
|
+
return { ok: false, refusal: "too-long" };
|
|
436
|
+
}
|
|
437
|
+
const provenance = provenanceOf(record.provenance);
|
|
438
|
+
return {
|
|
439
|
+
ok: true,
|
|
440
|
+
result: {
|
|
441
|
+
text,
|
|
442
|
+
...record.verdict !== void 0 ? { verdict: record.verdict } : {},
|
|
443
|
+
...provenance !== void 0 ? { provenance } : {}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function checkAiExplanation(raw, request) {
|
|
448
|
+
const read = readAiTextResult(raw);
|
|
449
|
+
if (!read.ok) {
|
|
450
|
+
return read;
|
|
451
|
+
}
|
|
452
|
+
if (read.result.verdict !== void 0 && read.result.verdict !== request.grade.category) {
|
|
453
|
+
return { ok: false, refusal: "contradicts-grade" };
|
|
454
|
+
}
|
|
455
|
+
return read;
|
|
456
|
+
}
|
|
457
|
+
function checkAiHint(raw, request) {
|
|
458
|
+
const read = readAiTextResult(raw);
|
|
459
|
+
if (!read.ok) {
|
|
460
|
+
return read;
|
|
461
|
+
}
|
|
462
|
+
if (hintRevealsAnswer(request.facts, read.result.text)) {
|
|
463
|
+
return { ok: false, refusal: "reveals-answer" };
|
|
464
|
+
}
|
|
465
|
+
return read;
|
|
466
|
+
}
|
|
467
|
+
function fold(text) {
|
|
468
|
+
const folded = text.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
469
|
+
return folded === "" ? [] : folded.split(" ");
|
|
470
|
+
}
|
|
471
|
+
function containsWords(haystack, needle) {
|
|
472
|
+
if (needle.length === 0 || needle.length > haystack.length) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
for (let start = 0; start + needle.length <= haystack.length; start += 1) {
|
|
476
|
+
let match = true;
|
|
477
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
478
|
+
if (haystack[start + offset] !== needle[offset]) {
|
|
479
|
+
match = false;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (match) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
var isShort = (words) => words.length === 1 && [...words[0]].length < 4;
|
|
490
|
+
function neighbours(passage, position) {
|
|
491
|
+
const marker = `[${position}]`;
|
|
492
|
+
const at = passage.indexOf(marker);
|
|
493
|
+
if (at === -1) {
|
|
494
|
+
return {};
|
|
495
|
+
}
|
|
496
|
+
const strip = (text) => text.replace(/\[\d+\]/g, " ");
|
|
497
|
+
const left = fold(strip(passage.slice(0, at)));
|
|
498
|
+
const right = fold(strip(passage.slice(at + marker.length)));
|
|
499
|
+
const before = left[left.length - 1];
|
|
500
|
+
const after = right[0];
|
|
501
|
+
return {
|
|
502
|
+
...before !== void 0 ? { before } : {},
|
|
503
|
+
...after !== void 0 ? { after } : {}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function hintRevealsAnswer(facts, hint) {
|
|
507
|
+
const words = fold(hint);
|
|
508
|
+
const reveals = (answer, passage, position) => {
|
|
509
|
+
const needle = fold(answer);
|
|
510
|
+
if (needle.length === 0) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
if (!isShort(needle) || passage === void 0 || position === void 0) {
|
|
514
|
+
return containsWords(words, needle);
|
|
515
|
+
}
|
|
516
|
+
const { before, after } = neighbours(passage, position);
|
|
517
|
+
if (before === void 0 && after === void 0) {
|
|
518
|
+
return containsWords(words, needle);
|
|
519
|
+
}
|
|
520
|
+
return before !== void 0 && containsWords(words, [before, ...needle]) || after !== void 0 && containsWords(words, [...needle, after]);
|
|
521
|
+
};
|
|
522
|
+
switch (facts.activityType) {
|
|
523
|
+
case "multiple-choice":
|
|
524
|
+
return facts.options.some((option) => option.correct === true && reveals(option.text));
|
|
525
|
+
case "fill-in-the-blanks":
|
|
526
|
+
return facts.blanks.some(
|
|
527
|
+
(blank) => (blank.accepted ?? []).some((answer) => reveals(answer, facts.passage, blank.position))
|
|
528
|
+
);
|
|
529
|
+
case "gap-select":
|
|
530
|
+
return facts.gaps.some(
|
|
531
|
+
(gap) => gap.answer !== null && reveals(gap.answer, facts.passage, gap.position)
|
|
532
|
+
);
|
|
533
|
+
case "dictation":
|
|
534
|
+
return false;
|
|
535
|
+
default:
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
138
539
|
|
|
139
540
|
// src/content-hash.ts
|
|
140
541
|
function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
@@ -734,11 +1135,12 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
734
1135
|
if (draft.timeline !== void 0 && draft.timeline !== null) {
|
|
735
1136
|
issues.push(...checkTimeline(draft));
|
|
736
1137
|
}
|
|
737
|
-
|
|
1138
|
+
const captions = groupCaptionsField(draft, (captionsUrl) => !isUnwritten(captionsUrl));
|
|
1139
|
+
if (captions !== void 0) {
|
|
738
1140
|
issues.push(
|
|
739
1141
|
issue(
|
|
740
1142
|
"dc_captions_not_allowed",
|
|
741
|
-
["stimulus", "media",
|
|
1143
|
+
["stimulus", "media", captions],
|
|
742
1144
|
GROUP_CAPTIONS_REVEAL_DICTATION
|
|
743
1145
|
)
|
|
744
1146
|
);
|
|
@@ -1218,11 +1620,11 @@ function normalise(found) {
|
|
|
1218
1620
|
}
|
|
1219
1621
|
|
|
1220
1622
|
// src/redact.ts
|
|
1221
|
-
function mergePolicy(
|
|
1623
|
+
function mergePolicy(base2, overrides) {
|
|
1222
1624
|
if (overrides === void 0) {
|
|
1223
|
-
return
|
|
1625
|
+
return base2;
|
|
1224
1626
|
}
|
|
1225
|
-
const merged = { ...
|
|
1627
|
+
const merged = { ...base2 };
|
|
1226
1628
|
for (const [key, override] of Object.entries(overrides)) {
|
|
1227
1629
|
const current = merged[key];
|
|
1228
1630
|
merged[key] = typeof override === "object" && typeof current === "object" ? { ...current, ...override } : override;
|
|
@@ -1486,7 +1888,11 @@ function composeTimelineScore(entryKey, items, policy) {
|
|
|
1486
1888
|
return composeAssessmentScore([{ id: entryKey, weight: 1, items: own }], policy);
|
|
1487
1889
|
}
|
|
1488
1890
|
export {
|
|
1891
|
+
AI_EXPLANATION_TYPES,
|
|
1892
|
+
AI_HINT_TYPES,
|
|
1893
|
+
AI_TEXT_MAX_LENGTH,
|
|
1489
1894
|
ActivitySchemaError,
|
|
1895
|
+
AiPermissionsSchema,
|
|
1490
1896
|
BlankConfigSchema,
|
|
1491
1897
|
DEFAULT_PASS_THRESHOLD,
|
|
1492
1898
|
DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
|
|
@@ -1554,11 +1960,19 @@ export {
|
|
|
1554
1960
|
WrittenResponseRubricSchema,
|
|
1555
1961
|
XAPIVerb,
|
|
1556
1962
|
XAPI_VERB_DISPLAY,
|
|
1963
|
+
aiAllowedByContent,
|
|
1964
|
+
aiExplanationRequest,
|
|
1965
|
+
aiGradeOf,
|
|
1966
|
+
aiHintRequest,
|
|
1967
|
+
aiSupports,
|
|
1557
1968
|
alignDictation,
|
|
1558
1969
|
alignReadAloud,
|
|
1559
1970
|
assertRedacted,
|
|
1560
1971
|
assertRedactedItemGroup,
|
|
1972
|
+
buildAiFacts,
|
|
1561
1973
|
canonicalJson,
|
|
1974
|
+
checkAiExplanation,
|
|
1975
|
+
checkAiHint,
|
|
1562
1976
|
classifyBand,
|
|
1563
1977
|
composeAssessmentScore,
|
|
1564
1978
|
composeTimelineScore,
|
|
@@ -1588,6 +2002,7 @@ export {
|
|
|
1588
2002
|
gte,
|
|
1589
2003
|
hasGrade,
|
|
1590
2004
|
hashSeed,
|
|
2005
|
+
hintRevealsAnswer,
|
|
1591
2006
|
inspectWav,
|
|
1592
2007
|
isInteractiveVideoItemType,
|
|
1593
2008
|
isItemGroup,
|
|
@@ -1601,6 +2016,7 @@ export {
|
|
|
1601
2016
|
outcomeFromUnscorable,
|
|
1602
2017
|
planAttempt,
|
|
1603
2018
|
planMediaBudgets,
|
|
2019
|
+
readAiTextResult,
|
|
1604
2020
|
readAloudJsonSchema,
|
|
1605
2021
|
readAloudType,
|
|
1606
2022
|
readMediaProgress,
|