@intellectif/lk-core 0.9.0 → 0.10.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/CHANGELOG.md +38 -0
- package/dist/{activity-CPtJUBek.d.cts → activity-B2QxK15j.d.cts} +177 -2
- package/dist/{activity-CPtJUBek.d.ts → activity-B2QxK15j.d.ts} +177 -2
- package/dist/{chunk-YVZCFUGP.cjs → chunk-3FEAEO3D.cjs} +18 -13
- package/dist/chunk-3FEAEO3D.cjs.map +1 -0
- package/dist/{chunk-CVRALNUK.cjs → chunk-7ODQRNR4.cjs} +618 -45
- package/dist/chunk-7ODQRNR4.cjs.map +1 -0
- package/dist/{chunk-AHEUYOT3.js → chunk-FIS5KBCE.js} +670 -97
- package/dist/chunk-FIS5KBCE.js.map +1 -0
- package/dist/{chunk-ELTP5P6V.js → chunk-IXXMYJI7.js} +2 -2
- package/dist/{chunk-MOLL5KXO.cjs → chunk-VBM5H6P5.cjs} +8 -8
- package/dist/{chunk-MOLL5KXO.cjs.map → chunk-VBM5H6P5.cjs.map} +1 -1
- package/dist/{chunk-RL2PQLCY.cjs → chunk-VTTRXGDD.cjs} +4 -4
- package/dist/{chunk-RL2PQLCY.cjs.map → chunk-VTTRXGDD.cjs.map} +1 -1
- package/dist/{chunk-6DM2H6BD.js → chunk-XGCTAQSS.js} +7 -2
- package/dist/chunk-XGCTAQSS.js.map +1 -0
- package/dist/{chunk-QX7P3CHP.js → chunk-ZTY4UIUD.js} +2 -2
- package/dist/{index-BvrA8nIV.d.ts → index-B_scInLM.d.cts} +321 -3
- package/dist/{index-gIN564mV.d.cts → index-CRKtWP5G.d.ts} +321 -3
- package/dist/index.cjs +53 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +25 -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 +24 -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-6DM2H6BD.js.map +0 -1
- package/dist/chunk-AHEUYOT3.js.map +0 -1
- package/dist/chunk-CVRALNUK.cjs.map +0 -1
- package/dist/chunk-YVZCFUGP.cjs.map +0 -1
- /package/dist/{chunk-ELTP5P6V.js.map → chunk-IXXMYJI7.js.map} +0 -0
- /package/dist/{chunk-QX7P3CHP.js.map → chunk-ZTY4UIUD.js.map} +0 -0
|
@@ -179,19 +179,19 @@ var FillInTheBlanksDataSchema = z3.looseObject({
|
|
|
179
179
|
difficultyLevel: z3.literal([1, 2, 3, 4, 5]).optional()
|
|
180
180
|
}).refine(
|
|
181
181
|
(data) => {
|
|
182
|
-
const
|
|
182
|
+
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
183
183
|
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
184
184
|
const id = match[1];
|
|
185
|
-
|
|
185
|
+
placeholderCounts2.set(id, (placeholderCounts2.get(id) ?? 0) + 1);
|
|
186
186
|
}
|
|
187
187
|
const blankIds = data.blanks.map((blank) => blank.id);
|
|
188
188
|
if (new Set(blankIds).size !== blankIds.length) {
|
|
189
189
|
return false;
|
|
190
190
|
}
|
|
191
|
-
if (
|
|
191
|
+
if (placeholderCounts2.size !== blankIds.length) {
|
|
192
192
|
return false;
|
|
193
193
|
}
|
|
194
|
-
return blankIds.every((id) =>
|
|
194
|
+
return blankIds.every((id) => placeholderCounts2.get(id) === 1);
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
@@ -199,25 +199,29 @@ var FillInTheBlanksDataSchema = z3.looseObject({
|
|
|
199
199
|
}
|
|
200
200
|
);
|
|
201
201
|
|
|
202
|
-
// src/count-words.ts
|
|
203
|
-
function countWords(text) {
|
|
204
|
-
if (typeof text !== "string") {
|
|
205
|
-
return 0;
|
|
206
|
-
}
|
|
207
|
-
const trimmed = text.trim();
|
|
208
|
-
if (trimmed === "") {
|
|
209
|
-
return 0;
|
|
210
|
-
}
|
|
211
|
-
return trimmed.split(/\s+/).length;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
202
|
// src/schemas/multiple-choice.ts
|
|
215
203
|
import { z as z4 } from "zod/v4";
|
|
204
|
+
var MultipleChoiceOptionMediaSchema = z4.looseObject({
|
|
205
|
+
// No `embed`: an iframe swallows the click that selects the option, so the
|
|
206
|
+
// learner could not choose it. No `video`: a native control bar inside the
|
|
207
|
+
// option's label eats the same click.
|
|
208
|
+
type: z4.enum(["image", "audio"]),
|
|
209
|
+
url: MediaUrlSchema,
|
|
210
|
+
alt: z4.string().min(1).optional(),
|
|
211
|
+
captionsUrl: MediaUrlSchema.optional()
|
|
212
|
+
}).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
|
|
213
|
+
error: "An image option requires a non-empty `alt`.",
|
|
214
|
+
path: ["alt"]
|
|
215
|
+
}).refine((media) => !("playback" in media), {
|
|
216
|
+
error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
|
|
217
|
+
path: ["playback"]
|
|
218
|
+
});
|
|
216
219
|
var MultipleChoiceOptionSchema = z4.looseObject({
|
|
217
220
|
id: z4.string().min(1),
|
|
218
221
|
text: z4.string().min(1),
|
|
219
222
|
isCorrect: z4.boolean(),
|
|
220
|
-
feedback: z4.string().optional()
|
|
223
|
+
feedback: z4.string().optional(),
|
|
224
|
+
media: MultipleChoiceOptionMediaSchema.optional()
|
|
221
225
|
});
|
|
222
226
|
var MultipleChoiceDataSchema = z4.looseObject({
|
|
223
227
|
schemaVersion: z4.literal("1.0"),
|
|
@@ -250,97 +254,243 @@ var MultipleChoiceDataSchema = z4.looseObject({
|
|
|
250
254
|
path: ["options"]
|
|
251
255
|
});
|
|
252
256
|
|
|
253
|
-
// src/
|
|
257
|
+
// src/count-words.ts
|
|
258
|
+
function countWords(text) {
|
|
259
|
+
if (typeof text !== "string") {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
const trimmed = text.trim();
|
|
263
|
+
if (trimmed === "") {
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
return trimmed.split(/\s+/).length;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/schemas/gap-select.ts
|
|
254
270
|
import { z as z5 } from "zod/v4";
|
|
255
|
-
var
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
weight: z5.number().min(0)
|
|
259
|
-
});
|
|
260
|
-
var WrittenResponseRubricSchema = z5.looseObject({
|
|
261
|
-
label: z5.string().optional(),
|
|
262
|
-
criteria: z5.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
271
|
+
var GapSelectChoiceSchema = z5.looseObject({
|
|
272
|
+
id: z5.string().min(1),
|
|
273
|
+
text: z5.string().min(1)
|
|
263
274
|
});
|
|
264
|
-
var
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
weight: z5.number().min(0)
|
|
275
|
+
var GapSelectBankSchema = z5.looseObject({
|
|
276
|
+
id: z5.string().min(1),
|
|
277
|
+
choices: z5.array(GapSelectChoiceSchema).min(2)
|
|
268
278
|
});
|
|
269
|
-
var
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
var GapSelectGapSchema = z5.looseObject({
|
|
280
|
+
id: z5.string().min(1),
|
|
281
|
+
choices: z5.array(GapSelectChoiceSchema).min(2).optional(),
|
|
282
|
+
bankId: z5.string().min(1).optional(),
|
|
283
|
+
correctChoiceId: z5.string().min(1),
|
|
284
|
+
feedback: z5.string().optional()
|
|
272
285
|
});
|
|
273
|
-
|
|
286
|
+
function resolveChoices(gap, banks) {
|
|
287
|
+
if (gap.choices !== void 0) {
|
|
288
|
+
return gap.bankId === void 0 ? gap.choices : void 0;
|
|
289
|
+
}
|
|
290
|
+
if (gap.bankId === void 0) {
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
return banks.find((bank) => bank.id === gap.bankId)?.choices;
|
|
294
|
+
}
|
|
295
|
+
function placeholderCounts(passage) {
|
|
296
|
+
const counts = /* @__PURE__ */ new Map();
|
|
297
|
+
for (const match of passage.matchAll(PLACEHOLDER_RE)) {
|
|
298
|
+
const id = match[1];
|
|
299
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
300
|
+
}
|
|
301
|
+
return counts;
|
|
302
|
+
}
|
|
303
|
+
var GapSelectDataSchema = z5.looseObject({
|
|
274
304
|
schemaVersion: z5.literal("1.0"),
|
|
275
|
-
type: z5.literal("
|
|
305
|
+
type: z5.literal("gap-select"),
|
|
276
306
|
id: z5.string().min(1),
|
|
277
307
|
title: z5.string().min(1),
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
308
|
+
passage: z5.string().min(1),
|
|
309
|
+
passageHtml: z5.string().optional(),
|
|
310
|
+
gaps: z5.array(GapSelectGapSchema).min(1),
|
|
311
|
+
banks: z5.array(GapSelectBankSchema).optional(),
|
|
312
|
+
scoringStrategy: z5.enum(["all-or-nothing", "partial"]),
|
|
313
|
+
presentation: z5.literal("dropdown").optional(),
|
|
314
|
+
shuffleChoices: z5.boolean().optional(),
|
|
284
315
|
media: MediaSchema.optional(),
|
|
285
316
|
feedback: FeedbackSchema.optional(),
|
|
286
317
|
passThreshold: z5.number().min(0).max(1).optional(),
|
|
287
318
|
locale: z5.string().optional(),
|
|
288
319
|
learningObjectives: z5.array(z5.string()).optional(),
|
|
289
320
|
difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional()
|
|
321
|
+
}).refine(
|
|
322
|
+
(data) => {
|
|
323
|
+
const ids = (data.banks ?? []).map((bank) => bank.id);
|
|
324
|
+
return new Set(ids).size === ids.length;
|
|
325
|
+
},
|
|
326
|
+
{ error: "Word bank ids must be unique.", path: ["banks"] }
|
|
327
|
+
).refine(
|
|
328
|
+
(data) => {
|
|
329
|
+
const ids = data.gaps.map((gap) => gap.id);
|
|
330
|
+
return new Set(ids).size === ids.length;
|
|
331
|
+
},
|
|
332
|
+
{ error: "Gap ids must be unique.", path: ["gaps"] }
|
|
333
|
+
).refine(
|
|
334
|
+
(data) => {
|
|
335
|
+
const counts = placeholderCounts(data.passage);
|
|
336
|
+
const ids = data.gaps.map((gap) => gap.id);
|
|
337
|
+
return counts.size === new Set(ids).size && ids.every((id) => counts.get(id) === 1) && [...counts.values()].every((count) => count === 1);
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
error: "Every gap must appear exactly once in the passage as {{gap_id}}, and every {{gap_id}} must have a gap.",
|
|
341
|
+
path: ["passage"]
|
|
342
|
+
}
|
|
343
|
+
).refine(
|
|
344
|
+
(data) => data.gaps.every((gap) => gap.choices === void 0 !== (gap.bankId === void 0)),
|
|
345
|
+
{
|
|
346
|
+
error: "Each gap needs exactly one choice source: its own `choices`, or a `bankId`.",
|
|
347
|
+
path: ["gaps"]
|
|
348
|
+
}
|
|
349
|
+
).refine(
|
|
350
|
+
(data) => {
|
|
351
|
+
const bankIds = new Set((data.banks ?? []).map((bank) => bank.id));
|
|
352
|
+
return data.gaps.every((gap) => gap.bankId === void 0 || bankIds.has(gap.bankId));
|
|
353
|
+
},
|
|
354
|
+
{ error: "A gap references a `bankId` that no word bank defines.", path: ["gaps"] }
|
|
355
|
+
).refine(
|
|
356
|
+
(data) => {
|
|
357
|
+
const banks = data.banks ?? [];
|
|
358
|
+
return data.gaps.every((gap) => {
|
|
359
|
+
const choices = resolveChoices(gap, banks);
|
|
360
|
+
if (choices === void 0) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
const ids = choices.map((choice) => choice.id);
|
|
364
|
+
return new Set(ids).size === ids.length && ids.includes(gap.correctChoiceId);
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
|
|
369
|
+
path: ["gaps"]
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// src/schemas/written-response.ts
|
|
374
|
+
import { z as z6 } from "zod/v4";
|
|
375
|
+
var WrittenResponseRubricCriterionSchema = z6.looseObject({
|
|
376
|
+
name: z6.string().min(1),
|
|
377
|
+
description: z6.string().optional(),
|
|
378
|
+
weight: z6.number().min(0)
|
|
379
|
+
});
|
|
380
|
+
var WrittenResponseRubricSchema = z6.looseObject({
|
|
381
|
+
label: z6.string().optional(),
|
|
382
|
+
criteria: z6.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
383
|
+
});
|
|
384
|
+
var RedactedWrittenResponseRubricCriterionSchema = z6.strictObject({
|
|
385
|
+
name: z6.string().min(1),
|
|
386
|
+
description: z6.string().optional(),
|
|
387
|
+
weight: z6.number().min(0)
|
|
388
|
+
});
|
|
389
|
+
var RedactedWrittenResponseRubricSchema = z6.strictObject({
|
|
390
|
+
label: z6.string().optional(),
|
|
391
|
+
criteria: z6.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
|
|
392
|
+
});
|
|
393
|
+
var WrittenResponseDataSchema = z6.looseObject({
|
|
394
|
+
schemaVersion: z6.literal("1.0"),
|
|
395
|
+
type: z6.literal("written-response"),
|
|
396
|
+
id: z6.string().min(1),
|
|
397
|
+
title: z6.string().min(1),
|
|
398
|
+
prompt: z6.string(),
|
|
399
|
+
promptHtml: z6.string().optional(),
|
|
400
|
+
minWords: z6.number().int().min(0),
|
|
401
|
+
maxWords: z6.number().int().min(1),
|
|
402
|
+
rubric: WrittenResponseRubricSchema.optional(),
|
|
403
|
+
languageTarget: z6.string().optional(),
|
|
404
|
+
media: MediaSchema.optional(),
|
|
405
|
+
feedback: FeedbackSchema.optional(),
|
|
406
|
+
passThreshold: z6.number().min(0).max(1).optional(),
|
|
407
|
+
locale: z6.string().optional(),
|
|
408
|
+
learningObjectives: z6.array(z6.string()).optional(),
|
|
409
|
+
difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional()
|
|
290
410
|
}).refine((data) => data.maxWords >= data.minWords, {
|
|
291
411
|
error: "maxWords must be greater than or equal to minWords.",
|
|
292
412
|
path: ["maxWords"]
|
|
293
413
|
});
|
|
294
414
|
|
|
295
415
|
// src/schemas/redacted.ts
|
|
296
|
-
import { z as
|
|
416
|
+
import { z as z7 } from "zod/v4";
|
|
297
417
|
var redactedBase = {
|
|
298
418
|
/** Marker distinguishing a redacted projection from full activity data. */
|
|
299
|
-
redacted:
|
|
419
|
+
redacted: z7.literal(true),
|
|
300
420
|
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
301
|
-
slotKey:
|
|
302
|
-
schemaVersion:
|
|
303
|
-
id:
|
|
304
|
-
title:
|
|
421
|
+
slotKey: z7.string().min(1).optional(),
|
|
422
|
+
schemaVersion: z7.literal("1.0"),
|
|
423
|
+
id: z7.string().min(1),
|
|
424
|
+
title: z7.string().min(1),
|
|
305
425
|
media: RedactedMediaSchema.optional(),
|
|
306
|
-
passThreshold:
|
|
307
|
-
locale:
|
|
308
|
-
learningObjectives:
|
|
309
|
-
difficultyLevel:
|
|
426
|
+
passThreshold: z7.number().min(0).max(1).optional(),
|
|
427
|
+
locale: z7.string().optional(),
|
|
428
|
+
learningObjectives: z7.array(z7.string()).optional(),
|
|
429
|
+
difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional()
|
|
310
430
|
};
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
431
|
+
var RedactedMultipleChoiceOptionMediaSchema = z7.strictObject({
|
|
432
|
+
type: z7.enum(["image", "audio"]),
|
|
433
|
+
url: z7.string().min(1),
|
|
434
|
+
alt: z7.string().min(1).optional(),
|
|
435
|
+
captionsUrl: z7.string().min(1).optional()
|
|
436
|
+
});
|
|
437
|
+
var RedactedMultipleChoiceOptionSchema = z7.strictObject({
|
|
438
|
+
id: z7.string().min(1),
|
|
439
|
+
text: z7.string().min(1),
|
|
440
|
+
media: RedactedMultipleChoiceOptionMediaSchema.optional()
|
|
314
441
|
});
|
|
315
|
-
var RedactedMultipleChoiceDataSchema =
|
|
442
|
+
var RedactedMultipleChoiceDataSchema = z7.strictObject({
|
|
316
443
|
...redactedBase,
|
|
317
|
-
type:
|
|
318
|
-
question:
|
|
319
|
-
questionHtml:
|
|
320
|
-
mode:
|
|
321
|
-
options:
|
|
322
|
-
shuffle:
|
|
444
|
+
type: z7.literal("multiple-choice"),
|
|
445
|
+
question: z7.string().min(1),
|
|
446
|
+
questionHtml: z7.string().optional(),
|
|
447
|
+
mode: z7.enum(["single", "multi"]),
|
|
448
|
+
options: z7.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
|
|
449
|
+
shuffle: z7.boolean().optional()
|
|
323
450
|
});
|
|
324
|
-
var RedactedBlankConfigSchema =
|
|
325
|
-
id:
|
|
326
|
-
hint:
|
|
451
|
+
var RedactedBlankConfigSchema = z7.strictObject({
|
|
452
|
+
id: z7.string().min(1),
|
|
453
|
+
hint: z7.string().optional()
|
|
454
|
+
});
|
|
455
|
+
var RedactedFillInTheBlanksDataSchema = z7.strictObject({
|
|
456
|
+
...redactedBase,
|
|
457
|
+
type: z7.literal("fill-in-the-blanks"),
|
|
458
|
+
passage: z7.string().min(1),
|
|
459
|
+
passageHtml: z7.string().optional(),
|
|
460
|
+
blanks: z7.array(RedactedBlankConfigSchema).min(1)
|
|
461
|
+
});
|
|
462
|
+
var RedactedGapSelectChoiceSchema = z7.strictObject({
|
|
463
|
+
id: z7.string().min(1),
|
|
464
|
+
text: z7.string().min(1)
|
|
327
465
|
});
|
|
328
|
-
var
|
|
466
|
+
var RedactedGapSelectBankSchema = z7.strictObject({
|
|
467
|
+
id: z7.string().min(1),
|
|
468
|
+
choices: z7.array(RedactedGapSelectChoiceSchema).min(2)
|
|
469
|
+
});
|
|
470
|
+
var RedactedGapSelectGapSchema = z7.strictObject({
|
|
471
|
+
id: z7.string().min(1),
|
|
472
|
+
choices: z7.array(RedactedGapSelectChoiceSchema).min(2).optional(),
|
|
473
|
+
bankId: z7.string().min(1).optional()
|
|
474
|
+
});
|
|
475
|
+
var RedactedGapSelectDataSchema = z7.strictObject({
|
|
329
476
|
...redactedBase,
|
|
330
|
-
type:
|
|
331
|
-
passage:
|
|
332
|
-
passageHtml:
|
|
333
|
-
|
|
477
|
+
type: z7.literal("gap-select"),
|
|
478
|
+
passage: z7.string().min(1),
|
|
479
|
+
passageHtml: z7.string().optional(),
|
|
480
|
+
gaps: z7.array(RedactedGapSelectGapSchema).min(1),
|
|
481
|
+
banks: z7.array(RedactedGapSelectBankSchema).optional(),
|
|
482
|
+
presentation: z7.literal("dropdown").optional(),
|
|
483
|
+
shuffleChoices: z7.boolean().optional()
|
|
334
484
|
});
|
|
335
|
-
var RedactedWrittenResponseDataSchema =
|
|
485
|
+
var RedactedWrittenResponseDataSchema = z7.strictObject({
|
|
336
486
|
...redactedBase,
|
|
337
|
-
type:
|
|
338
|
-
prompt:
|
|
339
|
-
promptHtml:
|
|
340
|
-
minWords:
|
|
341
|
-
maxWords:
|
|
487
|
+
type: z7.literal("written-response"),
|
|
488
|
+
prompt: z7.string(),
|
|
489
|
+
promptHtml: z7.string().optional(),
|
|
490
|
+
minWords: z7.number().int().min(0),
|
|
491
|
+
maxWords: z7.number().int().min(1),
|
|
342
492
|
rubric: RedactedWrittenResponseRubricSchema.optional(),
|
|
343
|
-
languageTarget:
|
|
493
|
+
languageTarget: z7.string().optional()
|
|
344
494
|
});
|
|
345
495
|
|
|
346
496
|
// src/scoring/text-match.ts
|
|
@@ -484,6 +634,7 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
484
634
|
mc_option_id_duplicate: "invalid",
|
|
485
635
|
mc_option_text_required: "incomplete",
|
|
486
636
|
mc_option_correctness_required: "incomplete",
|
|
637
|
+
mc_option_media_kind: "invalid",
|
|
487
638
|
mc_correct_option_required: "incomplete",
|
|
488
639
|
mc_single_mode_one_correct: "invalid",
|
|
489
640
|
// fill-in-the-blanks
|
|
@@ -500,6 +651,27 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
500
651
|
fib_placeholder_missing: "incomplete",
|
|
501
652
|
fib_placeholder_duplicate: "invalid",
|
|
502
653
|
fib_blanks_mismatch: "invalid",
|
|
654
|
+
// gap-select
|
|
655
|
+
gs_passage_required: "incomplete",
|
|
656
|
+
gs_gaps_required: "incomplete",
|
|
657
|
+
gs_gap_id_required: "invalid",
|
|
658
|
+
gs_gap_id_duplicate: "invalid",
|
|
659
|
+
gs_gap_missing: "incomplete",
|
|
660
|
+
gs_placeholder_missing: "incomplete",
|
|
661
|
+
gs_placeholder_duplicate: "invalid",
|
|
662
|
+
gs_gaps_mismatch: "invalid",
|
|
663
|
+
gs_presentation_invalid: "invalid",
|
|
664
|
+
gs_bank_id_required: "invalid",
|
|
665
|
+
gs_bank_id_duplicate: "invalid",
|
|
666
|
+
gs_bank_unknown: "invalid",
|
|
667
|
+
gs_choice_source_required: "incomplete",
|
|
668
|
+
gs_choice_source_conflict: "invalid",
|
|
669
|
+
gs_choices_too_few: "incomplete",
|
|
670
|
+
gs_choice_id_required: "invalid",
|
|
671
|
+
gs_choice_id_duplicate: "invalid",
|
|
672
|
+
gs_choice_text_required: "incomplete",
|
|
673
|
+
gs_correct_choice_required: "incomplete",
|
|
674
|
+
gs_correct_choice_unknown: "invalid",
|
|
503
675
|
// written-response
|
|
504
676
|
wr_prompt_required: "incomplete",
|
|
505
677
|
wr_min_words_required: "incomplete",
|
|
@@ -633,51 +805,42 @@ function checkSharedOptional(draft) {
|
|
|
633
805
|
}
|
|
634
806
|
var URL_POLICY = 'Use an https:, http:, data: or blob: address, or a path on the same site that starts with a single "/".';
|
|
635
807
|
var EMBED_URL = "An embed needs the provider's full http(s) embed address, such as https://www.youtube.com/embed/VIDEO_ID.";
|
|
636
|
-
function checkMedia(media) {
|
|
808
|
+
function checkMedia(media, prefix = ["media"], schema = MediaSchema) {
|
|
637
809
|
const issues = [];
|
|
638
|
-
const
|
|
810
|
+
const at = (...rest) => [...prefix, ...rest];
|
|
811
|
+
const parsed = schema.safeParse(media, { reportInput: true });
|
|
639
812
|
const schemaIssues = parsed.success ? [] : parsed.error.issues.filter((found) => !refusesEmpty(media, found));
|
|
640
813
|
const refusal = (field) => schemaIssues.find((found) => found.path[0] === field);
|
|
641
814
|
const typeUnset = isUnwritten(media.type);
|
|
642
815
|
if (typeUnset) {
|
|
643
|
-
issues.push(
|
|
644
|
-
issue("media_type_required", ["media", "type"], "Choose what kind of media this is.")
|
|
645
|
-
);
|
|
816
|
+
issues.push(issue("media_type_required", at("type"), "Choose what kind of media this is."));
|
|
646
817
|
}
|
|
647
818
|
if (isUnwritten(media.url)) {
|
|
648
|
-
issues.push(
|
|
649
|
-
issue("media_url_required", ["media", "url"], "Add the address of the media file.")
|
|
650
|
-
);
|
|
819
|
+
issues.push(issue("media_url_required", at("url"), "Add the address of the media file."));
|
|
651
820
|
} else {
|
|
652
821
|
const refused = refusal("url");
|
|
653
822
|
if (refused !== void 0) {
|
|
654
823
|
issues.push(
|
|
655
|
-
issue(
|
|
656
|
-
"media_url_invalid",
|
|
657
|
-
["media", "url"],
|
|
658
|
-
refused.code === "custom" ? EMBED_URL : URL_POLICY
|
|
659
|
-
)
|
|
824
|
+
issue("media_url_invalid", at("url"), refused.code === "custom" ? EMBED_URL : URL_POLICY)
|
|
660
825
|
);
|
|
661
826
|
}
|
|
662
827
|
}
|
|
663
828
|
const needsAlt = media.type === "image" || media.type === "embed";
|
|
664
829
|
const alt = media.alt;
|
|
665
830
|
if (needsAlt && isUnwritten(alt) || typeof alt === "string" && alt.trim() === "") {
|
|
666
|
-
issues.push(
|
|
667
|
-
issue("media_alt_required", ["media", "alt"], "Add a text description of the media.")
|
|
668
|
-
);
|
|
831
|
+
issues.push(issue("media_alt_required", at("alt"), "Add a text description of the media."));
|
|
669
832
|
}
|
|
670
833
|
const captions = media.captionsUrl;
|
|
671
834
|
if (typeof captions === "string" && captions.trim() === "") {
|
|
672
835
|
issues.push(
|
|
673
836
|
issue(
|
|
674
837
|
"media_url_required",
|
|
675
|
-
|
|
838
|
+
at("captionsUrl"),
|
|
676
839
|
"Add the address of the captions file, or remove the captions."
|
|
677
840
|
)
|
|
678
841
|
);
|
|
679
842
|
} else if (refusal("captionsUrl") !== void 0) {
|
|
680
|
-
issues.push(issue("media_url_invalid",
|
|
843
|
+
issues.push(issue("media_url_invalid", at("captionsUrl"), URL_POLICY));
|
|
681
844
|
}
|
|
682
845
|
for (const schemaIssue of schemaIssues) {
|
|
683
846
|
const field = schemaIssue.path[0];
|
|
@@ -687,7 +850,7 @@ function checkMedia(media) {
|
|
|
687
850
|
issues.push(
|
|
688
851
|
issue(
|
|
689
852
|
field === "playback" ? "media_playback_invalid" : "media_invalid",
|
|
690
|
-
|
|
853
|
+
at(...schemaIssue.path.map(String)),
|
|
691
854
|
schemaIssue.message
|
|
692
855
|
)
|
|
693
856
|
);
|
|
@@ -888,6 +1051,301 @@ function pairsOneToOne(placeholders, blanks) {
|
|
|
888
1051
|
return blankIds.every((id) => typeof id === "string" && placeholders.get(id) === 1);
|
|
889
1052
|
}
|
|
890
1053
|
|
|
1054
|
+
// src/authoring/gap-select.ts
|
|
1055
|
+
var MIN_CHOICES = 2;
|
|
1056
|
+
var gapSelectAuthoring = {
|
|
1057
|
+
createDraft: ({ newId }) => ({
|
|
1058
|
+
schemaVersion: "1.0",
|
|
1059
|
+
type: "gap-select",
|
|
1060
|
+
id: newId(),
|
|
1061
|
+
title: "",
|
|
1062
|
+
passage: "",
|
|
1063
|
+
gaps: [],
|
|
1064
|
+
scoringStrategy: "all-or-nothing"
|
|
1065
|
+
}),
|
|
1066
|
+
checkDraft: checkGapSelectDraft
|
|
1067
|
+
};
|
|
1068
|
+
function checkGapSelectDraft(draft) {
|
|
1069
|
+
const issues = checkIdentity(draft, "gap-select");
|
|
1070
|
+
const passage = draft.passage;
|
|
1071
|
+
if (isUnwritten(passage)) {
|
|
1072
|
+
issues.push(issue("gs_passage_required", ["passage"], "Write the passage."));
|
|
1073
|
+
}
|
|
1074
|
+
issues.push(...checkScoringStrategy(draft));
|
|
1075
|
+
if (!isUnset(draft.presentation) && draft.presentation !== "dropdown") {
|
|
1076
|
+
issues.push(
|
|
1077
|
+
issue(
|
|
1078
|
+
"gs_presentation_invalid",
|
|
1079
|
+
["presentation"],
|
|
1080
|
+
'The only presentation is "dropdown". Leave it out unless you mean to set it.'
|
|
1081
|
+
)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const banks = isUnset(draft.banks) ? [] : draft.banks;
|
|
1085
|
+
const bankIds = /* @__PURE__ */ new Set();
|
|
1086
|
+
if (Array.isArray(banks)) {
|
|
1087
|
+
issues.push(...checkBanks(banks, bankIds));
|
|
1088
|
+
}
|
|
1089
|
+
const gaps = isUnset(draft.gaps) ? [] : draft.gaps;
|
|
1090
|
+
const gapIds = [];
|
|
1091
|
+
if (Array.isArray(gaps)) {
|
|
1092
|
+
if (gaps.length === 0) {
|
|
1093
|
+
issues.push(
|
|
1094
|
+
issue(
|
|
1095
|
+
"gs_gaps_required",
|
|
1096
|
+
["gaps"],
|
|
1097
|
+
"Add at least one gap, and mark where it goes in the passage with {{id}}."
|
|
1098
|
+
)
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
issues.push(...checkGaps(gaps, banks, bankIds, gapIds));
|
|
1102
|
+
if (typeof passage === "string" && passage.trim() !== "") {
|
|
1103
|
+
issues.push(...checkPairing2(passage, gaps, gapIds));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
issues.push(...checkSharedOptional(draft));
|
|
1107
|
+
return issues;
|
|
1108
|
+
}
|
|
1109
|
+
function checkBanks(banks, bankIds) {
|
|
1110
|
+
const issues = [];
|
|
1111
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
1112
|
+
for (const [index, bank] of banks.entries()) {
|
|
1113
|
+
if (!isRecord(bank)) {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (isMissingId(bank.id)) {
|
|
1117
|
+
issues.push(
|
|
1118
|
+
issue("gs_bank_id_required", ["banks", index, "id"], `Word bank ${index + 1} has no id.`)
|
|
1119
|
+
);
|
|
1120
|
+
} else if (typeof bank.id === "string") {
|
|
1121
|
+
if (bankIds.has(bank.id)) {
|
|
1122
|
+
repeated.add(bank.id);
|
|
1123
|
+
}
|
|
1124
|
+
bankIds.add(bank.id);
|
|
1125
|
+
}
|
|
1126
|
+
const name = typeof bank.id === "string" && bank.id !== "" ? `"${bank.id}"` : index + 1;
|
|
1127
|
+
issues.push(...checkChoiceList(bank.choices, ["banks", index, "choices"], `word bank ${name}`));
|
|
1128
|
+
}
|
|
1129
|
+
for (const id of repeated) {
|
|
1130
|
+
issues.push(
|
|
1131
|
+
issue("gs_bank_id_duplicate", ["banks"], `More than one word bank has the id "${id}".`)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return issues;
|
|
1135
|
+
}
|
|
1136
|
+
function checkGaps(gaps, banks, bankIds, gapIds) {
|
|
1137
|
+
const issues = [];
|
|
1138
|
+
const atGaps = [];
|
|
1139
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
1140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1141
|
+
for (const [index, gap] of gaps.entries()) {
|
|
1142
|
+
if (!isRecord(gap)) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const ordinal = index + 1;
|
|
1146
|
+
const name = typeof gap.id === "string" && gap.id !== "" ? `"${gap.id}"` : ordinal;
|
|
1147
|
+
if (isMissingId(gap.id)) {
|
|
1148
|
+
issues.push(issue("gs_gap_id_required", ["gaps", index, "id"], `Gap ${ordinal} has no id.`));
|
|
1149
|
+
} else if (typeof gap.id === "string") {
|
|
1150
|
+
if (seen.has(gap.id)) {
|
|
1151
|
+
repeated.add(gap.id);
|
|
1152
|
+
}
|
|
1153
|
+
seen.add(gap.id);
|
|
1154
|
+
gapIds.push(gap.id);
|
|
1155
|
+
}
|
|
1156
|
+
const hasOwn = !isUnset(gap.choices);
|
|
1157
|
+
const hasBank = !isUnwritten(gap.bankId);
|
|
1158
|
+
const bankBlank = typeof gap.bankId === "string" && gap.bankId.trim() === "";
|
|
1159
|
+
if (bankBlank) {
|
|
1160
|
+
issues.push(
|
|
1161
|
+
issue(
|
|
1162
|
+
"gs_choice_source_required",
|
|
1163
|
+
["gaps", index, "bankId"],
|
|
1164
|
+
`Choose a word bank for gap ${name}, or remove the empty field.`
|
|
1165
|
+
)
|
|
1166
|
+
);
|
|
1167
|
+
} else if (hasOwn && hasBank) {
|
|
1168
|
+
atGaps.push(
|
|
1169
|
+
issue(
|
|
1170
|
+
"gs_choice_source_conflict",
|
|
1171
|
+
["gaps"],
|
|
1172
|
+
`Gap ${name} has both its own choices and a word bank. Use one or the other.`
|
|
1173
|
+
)
|
|
1174
|
+
);
|
|
1175
|
+
} else if (!hasOwn && !hasBank) {
|
|
1176
|
+
atGaps.push(
|
|
1177
|
+
issue(
|
|
1178
|
+
"gs_choice_source_required",
|
|
1179
|
+
["gaps"],
|
|
1180
|
+
`Give gap ${name} a list of choices, or point it at a word bank.`
|
|
1181
|
+
)
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
if (hasBank && typeof gap.bankId === "string" && !bankIds.has(gap.bankId)) {
|
|
1185
|
+
atGaps.push(
|
|
1186
|
+
issue(
|
|
1187
|
+
"gs_bank_unknown",
|
|
1188
|
+
["gaps"],
|
|
1189
|
+
`Gap ${name} uses the word bank "${gap.bankId}", which does not exist.`
|
|
1190
|
+
)
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
if (hasOwn) {
|
|
1194
|
+
issues.push(...checkChoiceList(gap.choices, ["gaps", index, "choices"], `gap ${name}`));
|
|
1195
|
+
}
|
|
1196
|
+
issues.push(...checkAnswerKey(gap, index, name, banks, bankIds));
|
|
1197
|
+
}
|
|
1198
|
+
for (const id of repeated) {
|
|
1199
|
+
atGaps.push(issue("gs_gap_id_duplicate", ["gaps"], `More than one gap has the id "${id}".`));
|
|
1200
|
+
}
|
|
1201
|
+
return [...issues, ...atGaps];
|
|
1202
|
+
}
|
|
1203
|
+
function checkAnswerKey(gap, index, name, banks, bankIds) {
|
|
1204
|
+
const key = gap.correctChoiceId;
|
|
1205
|
+
if (isUnwritten(key)) {
|
|
1206
|
+
return [
|
|
1207
|
+
issue(
|
|
1208
|
+
"gs_correct_choice_required",
|
|
1209
|
+
["gaps", index, "correctChoiceId"],
|
|
1210
|
+
`Mark the correct choice for gap ${name}.`
|
|
1211
|
+
)
|
|
1212
|
+
];
|
|
1213
|
+
}
|
|
1214
|
+
if (typeof key !== "string") {
|
|
1215
|
+
return [];
|
|
1216
|
+
}
|
|
1217
|
+
const choices = resolveChoices2(gap, banks, bankIds);
|
|
1218
|
+
if (choices === void 0) {
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
const offered = choices.some((choice) => isRecord(choice) && choice.id === key);
|
|
1222
|
+
return offered ? [] : [
|
|
1223
|
+
issue(
|
|
1224
|
+
"gs_correct_choice_unknown",
|
|
1225
|
+
["gaps"],
|
|
1226
|
+
`Gap ${name} is marked correct on "${key}", which is not one of its choices.`
|
|
1227
|
+
)
|
|
1228
|
+
];
|
|
1229
|
+
}
|
|
1230
|
+
function resolveChoices2(gap, banks, bankIds) {
|
|
1231
|
+
const hasOwn = !isUnset(gap.choices);
|
|
1232
|
+
const hasBank = !isUnwritten(gap.bankId);
|
|
1233
|
+
if (hasOwn === hasBank) {
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
if (hasOwn) {
|
|
1237
|
+
return Array.isArray(gap.choices) ? gap.choices : void 0;
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof gap.bankId !== "string" || !bankIds.has(gap.bankId) || !Array.isArray(banks)) {
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
const bank = banks.find((entry) => isRecord(entry) && entry.id === gap.bankId);
|
|
1243
|
+
return isRecord(bank) && Array.isArray(bank.choices) ? bank.choices : void 0;
|
|
1244
|
+
}
|
|
1245
|
+
function checkChoiceList(choices, path, owner) {
|
|
1246
|
+
const issues = [];
|
|
1247
|
+
const list = isUnset(choices) ? [] : choices;
|
|
1248
|
+
if (!Array.isArray(list)) {
|
|
1249
|
+
return issues;
|
|
1250
|
+
}
|
|
1251
|
+
if (list.length < MIN_CHOICES) {
|
|
1252
|
+
issues.push(
|
|
1253
|
+
issue(
|
|
1254
|
+
"gs_choices_too_few",
|
|
1255
|
+
path,
|
|
1256
|
+
`Give ${owner} at least ${MIN_CHOICES} choices to pick from.`
|
|
1257
|
+
)
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1261
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
1262
|
+
for (const [index, choice] of list.entries()) {
|
|
1263
|
+
if (!isRecord(choice)) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (isMissingId(choice.id)) {
|
|
1267
|
+
issues.push(
|
|
1268
|
+
issue("gs_choice_id_required", [...path, index, "id"], `Choice ${index + 1} has no id.`)
|
|
1269
|
+
);
|
|
1270
|
+
} else if (typeof choice.id === "string") {
|
|
1271
|
+
if (seen.has(choice.id)) {
|
|
1272
|
+
repeated.add(choice.id);
|
|
1273
|
+
}
|
|
1274
|
+
seen.add(choice.id);
|
|
1275
|
+
}
|
|
1276
|
+
if (isUnwritten(choice.text)) {
|
|
1277
|
+
issues.push(
|
|
1278
|
+
issue(
|
|
1279
|
+
"gs_choice_text_required",
|
|
1280
|
+
[...path, index, "text"],
|
|
1281
|
+
`Write the text of choice ${index + 1}.`
|
|
1282
|
+
)
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
for (const id of repeated) {
|
|
1287
|
+
issues.push(issue("gs_choice_id_duplicate", path, `More than one choice has the id "${id}".`));
|
|
1288
|
+
}
|
|
1289
|
+
return issues;
|
|
1290
|
+
}
|
|
1291
|
+
function checkPairing2(passage, gaps, gapIds) {
|
|
1292
|
+
const issues = [];
|
|
1293
|
+
const placeholders = /* @__PURE__ */ new Map();
|
|
1294
|
+
for (const match of passage.matchAll(PLACEHOLDER_RE)) {
|
|
1295
|
+
const id = match[1];
|
|
1296
|
+
placeholders.set(id, (placeholders.get(id) ?? 0) + 1);
|
|
1297
|
+
}
|
|
1298
|
+
const ids = new Set(gapIds);
|
|
1299
|
+
for (const [id, count] of placeholders) {
|
|
1300
|
+
if (count > 1) {
|
|
1301
|
+
issues.push(
|
|
1302
|
+
issue(
|
|
1303
|
+
"gs_placeholder_duplicate",
|
|
1304
|
+
["passage"],
|
|
1305
|
+
`{{${id}}} appears more than once in the passage. Each gap goes in one place.`
|
|
1306
|
+
)
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
if (!ids.has(id)) {
|
|
1310
|
+
issues.push(
|
|
1311
|
+
issue(
|
|
1312
|
+
"gs_gap_missing",
|
|
1313
|
+
["passage"],
|
|
1314
|
+
`The passage has {{${id}}}, but there is no gap with that id.`
|
|
1315
|
+
)
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
for (const id of ids) {
|
|
1320
|
+
if (!placeholders.has(id)) {
|
|
1321
|
+
issues.push(
|
|
1322
|
+
issue(
|
|
1323
|
+
"gs_placeholder_missing",
|
|
1324
|
+
["passage"],
|
|
1325
|
+
`Gap "${id}" is not in the passage. Put {{${id}}} where it goes.`
|
|
1326
|
+
)
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (issues.length === 0 && !pairsOneToOne2(placeholders, gaps)) {
|
|
1331
|
+
issues.push(
|
|
1332
|
+
issue(
|
|
1333
|
+
"gs_gaps_mismatch",
|
|
1334
|
+
["passage"],
|
|
1335
|
+
"The gaps and the {{id}} placeholders in the passage do not pair up one to one."
|
|
1336
|
+
)
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return issues;
|
|
1340
|
+
}
|
|
1341
|
+
function pairsOneToOne2(placeholders, gaps) {
|
|
1342
|
+
const ids = gaps.filter(isRecord).map((gap) => gap.id);
|
|
1343
|
+
if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
891
1349
|
// src/authoring/multiple-choice.ts
|
|
892
1350
|
var MIN_OPTIONS = 2;
|
|
893
1351
|
var MAX_OPTIONS = 26;
|
|
@@ -967,6 +1425,9 @@ function checkMultipleChoiceDraft(draft) {
|
|
|
967
1425
|
if (option.isCorrect === true) {
|
|
968
1426
|
correct += 1;
|
|
969
1427
|
}
|
|
1428
|
+
if (isRecord(option.media)) {
|
|
1429
|
+
issues.push(...checkOptionMedia(option.media, index, ordinal));
|
|
1430
|
+
}
|
|
970
1431
|
}
|
|
971
1432
|
for (const id of repeated) {
|
|
972
1433
|
issues.push(
|
|
@@ -994,6 +1455,20 @@ function checkMultipleChoiceDraft(draft) {
|
|
|
994
1455
|
issues.push(...checkSharedOptional(draft));
|
|
995
1456
|
return issues;
|
|
996
1457
|
}
|
|
1458
|
+
function checkOptionMedia(media, index, ordinal) {
|
|
1459
|
+
const path = ["options", index, "media"];
|
|
1460
|
+
const kind = media.type;
|
|
1461
|
+
if (kind === "video" || kind === "embed") {
|
|
1462
|
+
return [
|
|
1463
|
+
issue(
|
|
1464
|
+
"mc_option_media_kind",
|
|
1465
|
+
[...path, "type"],
|
|
1466
|
+
`Option ${ordinal} carries ${kind === "embed" ? "an embedded player" : "a video"}, which cannot be an option: its controls swallow the click that selects the answer. Use a picture or a recording.`
|
|
1467
|
+
)
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
return checkMedia(media, path, MultipleChoiceOptionMediaSchema);
|
|
1471
|
+
}
|
|
997
1472
|
|
|
998
1473
|
// src/authoring/written-response.ts
|
|
999
1474
|
var writtenResponseAuthoring = {
|
|
@@ -1181,6 +1656,38 @@ function scoreFillInTheBlanks(data, response) {
|
|
|
1181
1656
|
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
1182
1657
|
}
|
|
1183
1658
|
|
|
1659
|
+
// src/scoring/activity-scorers/gap-select.ts
|
|
1660
|
+
function choicesFor(data, gap) {
|
|
1661
|
+
if (gap.choices !== void 0) {
|
|
1662
|
+
return gap.choices;
|
|
1663
|
+
}
|
|
1664
|
+
return data.banks?.find((bank) => bank.id === gap.bankId)?.choices ?? [];
|
|
1665
|
+
}
|
|
1666
|
+
function scoreGapSelect(data, response) {
|
|
1667
|
+
const details = [];
|
|
1668
|
+
const perGapCorrect = [];
|
|
1669
|
+
for (const gap of data.gaps) {
|
|
1670
|
+
const raw = response.selections[gap.id];
|
|
1671
|
+
const selected = typeof raw === "string" ? raw : "";
|
|
1672
|
+
const choices = choicesFor(data, gap);
|
|
1673
|
+
const offered = choices.some((choice) => choice.id === selected);
|
|
1674
|
+
const answered = selected !== "" && offered;
|
|
1675
|
+
const correct = answered && selected === gap.correctChoiceId;
|
|
1676
|
+
perGapCorrect.push(correct);
|
|
1677
|
+
details.push({
|
|
1678
|
+
itemId: gap.id,
|
|
1679
|
+
correct,
|
|
1680
|
+
outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
|
|
1681
|
+
learnerResponse: [selected],
|
|
1682
|
+
correctResponse: [gap.correctChoiceId],
|
|
1683
|
+
weight: 1
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const correctGaps = perGapCorrect.filter(Boolean).length;
|
|
1687
|
+
const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perGapCorrect) : partialBlankStrategy(correctGaps, data.gaps.length);
|
|
1688
|
+
return { score: scoreValue, maxScore: 1, feedback: null, details };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1184
1691
|
// src/scoring/activity-scorers/multiple-choice.ts
|
|
1185
1692
|
function scoreMultipleChoice(data, response) {
|
|
1186
1693
|
const optionById = new Map(data.options.map((option) => [option.id, option]));
|
|
@@ -1280,6 +1787,38 @@ var MULTIPLE_CHOICE_FIELD_POLICY = {
|
|
|
1280
1787
|
id: "public",
|
|
1281
1788
|
text: "public",
|
|
1282
1789
|
isCorrect: "answer-key",
|
|
1790
|
+
feedback: "answer-key",
|
|
1791
|
+
// Public, and classified key by key rather than as one leaf — the lesson
|
|
1792
|
+
// `media`, `rubric` and `feedback` each taught: a scalar classification
|
|
1793
|
+
// assigns the author's object by reference without recursing, so an
|
|
1794
|
+
// unclassified key parked inside it survives redact() AND assertRedacted().
|
|
1795
|
+
// An option's picture or recording IS the thing the learner picks, so
|
|
1796
|
+
// stripping it would ship a row of blank options.
|
|
1797
|
+
media: {
|
|
1798
|
+
type: "public",
|
|
1799
|
+
url: "public",
|
|
1800
|
+
alt: "public",
|
|
1801
|
+
captionsUrl: "public"
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
var GAP_SELECT_FIELD_POLICY = {
|
|
1806
|
+
...SHARED_PUBLIC_FIELDS,
|
|
1807
|
+
passage: "public",
|
|
1808
|
+
passageHtml: "public",
|
|
1809
|
+
presentation: "public",
|
|
1810
|
+
shuffleChoices: "public",
|
|
1811
|
+
scoringStrategy: "answer-key",
|
|
1812
|
+
feedback: FEEDBACK_FIELD_POLICY,
|
|
1813
|
+
banks: {
|
|
1814
|
+
id: "public",
|
|
1815
|
+
choices: { id: "public", text: "public" }
|
|
1816
|
+
},
|
|
1817
|
+
gaps: {
|
|
1818
|
+
id: "public",
|
|
1819
|
+
bankId: "public",
|
|
1820
|
+
choices: { id: "public", text: "public" },
|
|
1821
|
+
correctChoiceId: "answer-key",
|
|
1283
1822
|
feedback: "answer-key"
|
|
1284
1823
|
}
|
|
1285
1824
|
};
|
|
@@ -1389,9 +1928,33 @@ var writtenResponseType = defineActivityType({
|
|
|
1389
1928
|
interactions: ["text-changed", "submitted"],
|
|
1390
1929
|
authoring: writtenResponseAuthoring
|
|
1391
1930
|
});
|
|
1931
|
+
var gapSelectType = defineActivityType({
|
|
1932
|
+
type: "gap-select",
|
|
1933
|
+
schema: GapSelectDataSchema,
|
|
1934
|
+
scoring: { kind: "sync", score: scoreGapSelect },
|
|
1935
|
+
isAnswered: (response) => Object.values(response?.selections ?? {}).some((choiceId) => choiceId !== ""),
|
|
1936
|
+
fieldPolicy: GAP_SELECT_FIELD_POLICY,
|
|
1937
|
+
redactedSchema: RedactedGapSelectDataSchema,
|
|
1938
|
+
interop: {
|
|
1939
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
1940
|
+
// `matching`, not `choice` or `fill-in`. The learner pairs a set of sources
|
|
1941
|
+
// (the gaps) with a set of targets (the choices), which is exactly what the
|
|
1942
|
+
// xAPI matching interaction describes, and it is the only built-in type
|
|
1943
|
+
// whose pattern can name WHICH gap took which answer. `fill-in` — what the
|
|
1944
|
+
// Fill-in-the-Blanks descriptor uses — would flatten the gaps into an
|
|
1945
|
+
// ordered list of strings and lose that.
|
|
1946
|
+
xapiInteractionType: "matching",
|
|
1947
|
+
correctResponsesPattern: (data) => [
|
|
1948
|
+
data.gaps.map((gap) => `${gap.id}[.]${gap.correctChoiceId}`).join("[,]")
|
|
1949
|
+
]
|
|
1950
|
+
},
|
|
1951
|
+
interactions: ["gap-selected", "submitted"],
|
|
1952
|
+
authoring: gapSelectAuthoring
|
|
1953
|
+
});
|
|
1392
1954
|
registerActivityType(multipleChoiceType);
|
|
1393
1955
|
registerActivityType(fillInTheBlanksType);
|
|
1394
1956
|
registerActivityType(writtenResponseType);
|
|
1957
|
+
registerActivityType(gapSelectType);
|
|
1395
1958
|
|
|
1396
1959
|
export {
|
|
1397
1960
|
ActivitySchemaError,
|
|
@@ -1411,16 +1974,26 @@ export {
|
|
|
1411
1974
|
issue,
|
|
1412
1975
|
isRecord,
|
|
1413
1976
|
refusesEmpty,
|
|
1414
|
-
|
|
1977
|
+
MultipleChoiceOptionMediaSchema,
|
|
1415
1978
|
MultipleChoiceOptionSchema,
|
|
1416
1979
|
MultipleChoiceDataSchema,
|
|
1980
|
+
countWords,
|
|
1981
|
+
GapSelectChoiceSchema,
|
|
1982
|
+
GapSelectBankSchema,
|
|
1983
|
+
GapSelectGapSchema,
|
|
1984
|
+
GapSelectDataSchema,
|
|
1417
1985
|
WrittenResponseRubricCriterionSchema,
|
|
1418
1986
|
WrittenResponseRubricSchema,
|
|
1419
1987
|
WrittenResponseDataSchema,
|
|
1988
|
+
RedactedMultipleChoiceOptionMediaSchema,
|
|
1420
1989
|
RedactedMultipleChoiceOptionSchema,
|
|
1421
1990
|
RedactedMultipleChoiceDataSchema,
|
|
1422
1991
|
RedactedBlankConfigSchema,
|
|
1423
1992
|
RedactedFillInTheBlanksDataSchema,
|
|
1993
|
+
RedactedGapSelectChoiceSchema,
|
|
1994
|
+
RedactedGapSelectBankSchema,
|
|
1995
|
+
RedactedGapSelectGapSchema,
|
|
1996
|
+
RedactedGapSelectDataSchema,
|
|
1424
1997
|
RedactedWrittenResponseDataSchema,
|
|
1425
1998
|
levenshteinDistance,
|
|
1426
1999
|
matchText,
|
|
@@ -1433,4 +2006,4 @@ export {
|
|
|
1433
2006
|
fillInTheBlanksType,
|
|
1434
2007
|
writtenResponseType
|
|
1435
2008
|
};
|
|
1436
|
-
//# sourceMappingURL=chunk-
|
|
2009
|
+
//# sourceMappingURL=chunk-FIS5KBCE.js.map
|