@intellectif/lk-core 1.1.0 → 1.2.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -1
  3. package/dist/{activity-MBOYLBjR.d.cts → activity-D705CXng.d.cts} +324 -2
  4. package/dist/{activity-MBOYLBjR.d.ts → activity-D705CXng.d.ts} +324 -2
  5. package/dist/ai-check.cjs +174 -12
  6. package/dist/ai-check.cjs.map +1 -1
  7. package/dist/ai-check.d.cts +15 -3
  8. package/dist/ai-check.d.ts +15 -3
  9. package/dist/ai-check.js +166 -4
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{chunk-TJJFKKH2.cjs → chunk-46BXIGLG.cjs} +4 -4
  12. package/dist/{chunk-TJJFKKH2.cjs.map → chunk-46BXIGLG.cjs.map} +1 -1
  13. package/dist/{chunk-VSH7ECQL.cjs → chunk-4YANYTFN.cjs} +43 -43
  14. package/dist/{chunk-VSH7ECQL.cjs.map → chunk-4YANYTFN.cjs.map} +1 -1
  15. package/dist/chunk-7KOWU4RO.js +2207 -0
  16. package/dist/chunk-7KOWU4RO.js.map +1 -0
  17. package/dist/{chunk-DSDXL3JO.cjs → chunk-7THXD7AN.cjs} +657 -197
  18. package/dist/chunk-7THXD7AN.cjs.map +1 -0
  19. package/dist/chunk-CYCTIXUN.cjs +118 -0
  20. package/dist/chunk-CYCTIXUN.cjs.map +1 -0
  21. package/dist/chunk-CYPA5BW5.js +118 -0
  22. package/dist/chunk-CYPA5BW5.js.map +1 -0
  23. package/dist/{chunk-SJVRUPJT.js → chunk-JTNDIBZO.js} +2 -2
  24. package/dist/{chunk-XS6U2ARZ.js → chunk-MV2N2WNS.js} +6 -112
  25. package/dist/chunk-MV2N2WNS.js.map +1 -0
  26. package/dist/chunk-MXI26H62.cjs +2207 -0
  27. package/dist/chunk-MXI26H62.cjs.map +1 -0
  28. package/dist/{chunk-FBKTION6.js → chunk-QS7B42ZR.js} +640 -180
  29. package/dist/chunk-QS7B42ZR.js.map +1 -0
  30. package/dist/{chunk-IARLJ5MW.js → chunk-QX2ERL72.js} +2 -2
  31. package/dist/{chunk-2JT2WUQ2.cjs → chunk-ZWXQSKCX.cjs} +14 -120
  32. package/dist/chunk-ZWXQSKCX.cjs.map +1 -0
  33. package/dist/{index-DlcqkM_L.d.ts → index-BChYtrEe.d.ts} +1 -1
  34. package/dist/{index-DlCW0XaQ.d.cts → index-VXt_v3YI.d.cts} +1 -1
  35. package/dist/index.cjs +68 -593
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +245 -60
  38. package/dist/index.d.ts +245 -60
  39. package/dist/index.js +68 -593
  40. package/dist/index.js.map +1 -1
  41. package/dist/schemas.cjs +5 -3
  42. package/dist/schemas.cjs.map +1 -1
  43. package/dist/schemas.d.cts +2 -2
  44. package/dist/schemas.d.ts +2 -2
  45. package/dist/schemas.js +5 -3
  46. package/dist/scoring.cjs +3 -3
  47. package/dist/scoring.d.cts +2 -2
  48. package/dist/scoring.d.ts +2 -2
  49. package/dist/scoring.js +2 -2
  50. package/dist/xapi.cjs +3 -3
  51. package/dist/xapi.d.cts +1 -1
  52. package/dist/xapi.d.ts +1 -1
  53. package/dist/xapi.js +2 -2
  54. package/package.json +1 -1
  55. package/dist/chunk-2JT2WUQ2.cjs.map +0 -1
  56. package/dist/chunk-DSDXL3JO.cjs.map +0 -1
  57. package/dist/chunk-FBKTION6.js.map +0 -1
  58. package/dist/chunk-LDFQUZII.cjs +0 -950
  59. package/dist/chunk-LDFQUZII.cjs.map +0 -1
  60. package/dist/chunk-NQHETFDA.js +0 -950
  61. package/dist/chunk-NQHETFDA.js.map +0 -1
  62. package/dist/chunk-XS6U2ARZ.js.map +0 -1
  63. /package/dist/{chunk-SJVRUPJT.js.map → chunk-JTNDIBZO.js.map} +0 -0
  64. /package/dist/{chunk-IARLJ5MW.js.map → chunk-QX2ERL72.js.map} +0 -0
@@ -289,7 +289,7 @@ var FOLDED = new Map(
289
289
  // TIBETAN MARK DELIMITER TSHEG BSTAR
290
290
  [65374, cp(12316)]
291
291
  // FULLWIDTH TILDE, which a Japanese keyboard types for the WAVE DASH
292
- ].map(([point, folded]) => [cp(point), folded])
292
+ ].map(([point, folded2]) => [cp(point), folded2])
293
293
  );
294
294
  var FOLDED_RE = new RegExp(`[${[...FOLDED.keys()].join("")}]`, "gu");
295
295
  var DRAWN_FORMAT_CHARACTERS = `${cp(1536)}-${cp(1541)}${cp(1757, 1807, 2192, 2193, 2274, 69821, 69837)}`;
@@ -660,10 +660,10 @@ var CARRIED_MARKS_RE = new RegExp(`^[ ${cp(1600)}]?(\\p{M}+)$`, "u");
660
660
  var SPACING_CHARACTER_RE = new RegExp(`[\\s${NEXT_LINE}]`, "u");
661
661
  var PUNCTUATION_CHARACTER_RE = /^\p{P}$/u;
662
662
  function foldCompatibility(character, offset, text) {
663
- const folded = character.normalize("NFKC");
664
- const carried = CARRIED_MARKS_RE.exec(folded);
663
+ const folded2 = character.normalize("NFKC");
664
+ const carried = CARRIED_MARKS_RE.exec(folded2);
665
665
  if (carried === null) {
666
- return COMPATIBILITY_JAMO.get(folded) ?? folded;
666
+ return COMPATIBILITY_JAMO.get(folded2) ?? folded2;
667
667
  }
668
668
  return PUNCTUATION_CHARACTER_RE.test(character) || offset === 0 || SPACING_CHARACTER_RE.test(text.charAt(offset - 1)) ? character : carried[1];
669
669
  }
@@ -917,8 +917,8 @@ var NOTHING_TO_REVEAL = { forms: [], long: false, before: "none", after: "none"
917
917
  function stripForReveal(text) {
918
918
  return stripPunctuation(text.replace(DIGIT_SEPARATOR_RE, ""));
919
919
  }
920
- function readingOf(written, working) {
921
- const writtenStripped = stripForReveal(written);
920
+ function readingOf(written2, working) {
921
+ const writtenStripped = stripForReveal(written2);
922
922
  const workingStripped = stripForReveal(working);
923
923
  const source = writtenStripped === "" ? workingStripped : writtenStripped;
924
924
  if (source === "") {
@@ -928,7 +928,7 @@ function readingOf(written, working) {
928
928
  const { before, after } = phraseOf(source, long ? "none" : "visible");
929
929
  const forms = [
930
930
  writtenStripped,
931
- spacePunctuation(written),
931
+ spacePunctuation(written2),
932
932
  workingStripped,
933
933
  spacePunctuation(working)
934
934
  ].map((form) => long ? form.replace(SPACE_BESIDE_UNSPACED_RE, "") : form);
@@ -945,13 +945,13 @@ function dictationNormalizer(tolerance) {
945
945
  if (isLongerThan(text, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
946
946
  result = { tooLong: true, normalized: null, reveal: NOTHING_TO_REVEAL };
947
947
  } else {
948
- const written = boundedPreStrip(text);
949
- const working = workingText(text, compiled, written);
948
+ const written2 = boundedPreStrip(text);
949
+ const working = workingText(text, compiled, written2);
950
950
  const normalized = stripPunctuation(working.text);
951
951
  result = {
952
952
  tooLong: working.cut || isLongerThan(normalized, DICTATION_MAX_TRANSCRIPT_LENGTH),
953
953
  normalized,
954
- reveal: readingOf(written.text, working.text)
954
+ reveal: readingOf(written2.text, working.text)
955
955
  };
956
956
  }
957
957
  measured.set(text, result);
@@ -1172,7 +1172,7 @@ function alignSequences(reference, attempt) {
1172
1172
  }
1173
1173
  function alignWords(reference, attempt) {
1174
1174
  const words = [];
1175
- let ordinal = 0;
1175
+ let ordinal2 = 0;
1176
1176
  for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
1177
1177
  if (step === STEP.extra) {
1178
1178
  words.push({
@@ -1183,11 +1183,11 @@ function alignWords(reference, attempt) {
1183
1183
  });
1184
1184
  continue;
1185
1185
  }
1186
- ordinal += 1;
1186
+ ordinal2 += 1;
1187
1187
  const referenceWord = reference[referenceIndex];
1188
1188
  if (step === STEP.missing) {
1189
1189
  words.push({
1190
- itemId: `w${ordinal}`,
1190
+ itemId: `w${ordinal2}`,
1191
1191
  reference: referenceWord,
1192
1192
  attempt: "",
1193
1193
  similarity: 0,
@@ -1197,7 +1197,7 @@ function alignWords(reference, attempt) {
1197
1197
  }
1198
1198
  const attemptWord = attempt[attemptIndex];
1199
1199
  words.push({
1200
- itemId: `w${ordinal}`,
1200
+ itemId: `w${ordinal2}`,
1201
1201
  reference: referenceWord,
1202
1202
  attempt: attemptWord,
1203
1203
  similarity: similarity(referenceWord, attemptWord),
@@ -1247,6 +1247,101 @@ var DeferredScoringError = class extends Error {
1247
1247
  activityType;
1248
1248
  };
1249
1249
 
1250
+ // src/count-words.ts
1251
+ function countWords(text) {
1252
+ if (typeof text !== "string") {
1253
+ return 0;
1254
+ }
1255
+ const trimmed = text.trim();
1256
+ if (trimmed === "") {
1257
+ return 0;
1258
+ }
1259
+ return trimmed.split(/\s+/).length;
1260
+ }
1261
+
1262
+ // src/scoring/text-match.ts
1263
+ var COMBINING_MARKS_RE = /\p{M}/gu;
1264
+ var PUNCTUATION_RE2 = /\p{P}/gu;
1265
+ function applyBaseline(value, policy) {
1266
+ let result = value;
1267
+ if (policy.trim !== false) {
1268
+ result = result.trim();
1269
+ }
1270
+ if (policy.caseSensitive !== true) {
1271
+ result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
1272
+ }
1273
+ return result;
1274
+ }
1275
+ function applyNormalization(value, policy) {
1276
+ let result = value;
1277
+ if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
1278
+ result = result.normalize(policy.normalize);
1279
+ }
1280
+ if (policy.ignorePunctuation === true) {
1281
+ result = result.replace(PUNCTUATION_RE2, "");
1282
+ }
1283
+ if (policy.collapseInnerWhitespace === true) {
1284
+ result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
1285
+ if (policy.trim !== false) {
1286
+ result = result.trim();
1287
+ }
1288
+ }
1289
+ return result;
1290
+ }
1291
+ function applyDiacriticFold(value) {
1292
+ return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
1293
+ }
1294
+ function levenshteinDistance(a, b, max) {
1295
+ if (a === b) return 0;
1296
+ if (Math.abs(a.length - b.length) > max) return max + 1;
1297
+ if (a.length === 0) return b.length;
1298
+ if (b.length === 0) return a.length;
1299
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1300
+ for (let i = 1; i <= a.length; i += 1) {
1301
+ const current = [i];
1302
+ let rowMin = i;
1303
+ for (let j = 1; j <= b.length; j += 1) {
1304
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1305
+ const value = Math.min(
1306
+ previous[j] + 1,
1307
+ current[j - 1] + 1,
1308
+ previous[j - 1] + cost
1309
+ );
1310
+ current.push(value);
1311
+ if (value < rowMin) rowMin = value;
1312
+ }
1313
+ if (rowMin > max) return max + 1;
1314
+ previous = current;
1315
+ }
1316
+ return previous[b.length];
1317
+ }
1318
+ function matchText(input, accepted, policy = {}) {
1319
+ const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
1320
+ const baselineInput = applyBaseline(input, policy);
1321
+ const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
1322
+ if (baselineAccepted.some((answer) => answer === baselineInput)) {
1323
+ return { matched: true, via: "exact" };
1324
+ }
1325
+ const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
1326
+ const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
1327
+ const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
1328
+ if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
1329
+ return { matched: true, via: "normalized" };
1330
+ }
1331
+ const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
1332
+ const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
1333
+ if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
1334
+ return { matched: true, via: "folded" };
1335
+ }
1336
+ const maxDistance = policy.levenshtein ?? 0;
1337
+ if (maxDistance > 0 && foldedInput.trim().length > 0 && foldedAccepted.some(
1338
+ (answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
1339
+ )) {
1340
+ return { matched: true, via: "fuzzy" };
1341
+ }
1342
+ return { matched: false, via: "none" };
1343
+ }
1344
+
1250
1345
  // src/schemas/multiple-choice.ts
1251
1346
  import { z as z4 } from "zod/v4";
1252
1347
 
@@ -1327,18 +1422,6 @@ var READ_ALOUD_MAX_DIMENSION_WEIGHT = 1e3;
1327
1422
  var SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
1328
1423
  var SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
1329
1424
 
1330
- // src/count-words.ts
1331
- function countWords(text) {
1332
- if (typeof text !== "string") {
1333
- return 0;
1334
- }
1335
- const trimmed = text.trim();
1336
- if (trimmed === "") {
1337
- return 0;
1338
- }
1339
- return trimmed.split(/\s+/).length;
1340
- }
1341
-
1342
1425
  // src/schemas/read-schema.ts
1343
1426
  var OWN_SCHEMAS = /* @__PURE__ */ new WeakSet();
1344
1427
  function ownSchemas(...schemas) {
@@ -1424,89 +1507,6 @@ function standardJsonSchemaOf(schema) {
1424
1507
  return generated !== null && typeof generated === "object" && !Array.isArray(generated) ? generated : void 0;
1425
1508
  }
1426
1509
 
1427
- // src/scoring/text-match.ts
1428
- var COMBINING_MARKS_RE = /\p{M}/gu;
1429
- var PUNCTUATION_RE2 = /\p{P}/gu;
1430
- function applyBaseline(value, policy) {
1431
- let result = value;
1432
- if (policy.trim !== false) {
1433
- result = result.trim();
1434
- }
1435
- if (policy.caseSensitive !== true) {
1436
- result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
1437
- }
1438
- return result;
1439
- }
1440
- function applyNormalization(value, policy) {
1441
- let result = value;
1442
- if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
1443
- result = result.normalize(policy.normalize);
1444
- }
1445
- if (policy.ignorePunctuation === true) {
1446
- result = result.replace(PUNCTUATION_RE2, "");
1447
- }
1448
- if (policy.collapseInnerWhitespace === true) {
1449
- result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
1450
- if (policy.trim !== false) {
1451
- result = result.trim();
1452
- }
1453
- }
1454
- return result;
1455
- }
1456
- function applyDiacriticFold(value) {
1457
- return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
1458
- }
1459
- function levenshteinDistance(a, b, max) {
1460
- if (a === b) return 0;
1461
- if (Math.abs(a.length - b.length) > max) return max + 1;
1462
- if (a.length === 0) return b.length;
1463
- if (b.length === 0) return a.length;
1464
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1465
- for (let i = 1; i <= a.length; i += 1) {
1466
- const current = [i];
1467
- let rowMin = i;
1468
- for (let j = 1; j <= b.length; j += 1) {
1469
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1470
- const value = Math.min(
1471
- previous[j] + 1,
1472
- current[j - 1] + 1,
1473
- previous[j - 1] + cost
1474
- );
1475
- current.push(value);
1476
- if (value < rowMin) rowMin = value;
1477
- }
1478
- if (rowMin > max) return max + 1;
1479
- previous = current;
1480
- }
1481
- return previous[b.length];
1482
- }
1483
- function matchText(input, accepted, policy = {}) {
1484
- const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
1485
- const baselineInput = applyBaseline(input, policy);
1486
- const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
1487
- if (baselineAccepted.some((answer) => answer === baselineInput)) {
1488
- return { matched: true, via: "exact" };
1489
- }
1490
- const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
1491
- const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
1492
- const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
1493
- if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
1494
- return { matched: true, via: "normalized" };
1495
- }
1496
- const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
1497
- const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
1498
- if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
1499
- return { matched: true, via: "folded" };
1500
- }
1501
- const maxDistance = policy.levenshtein ?? 0;
1502
- if (maxDistance > 0 && foldedInput.trim().length > 0 && foldedAccepted.some(
1503
- (answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
1504
- )) {
1505
- return { matched: true, via: "fuzzy" };
1506
- }
1507
- return { matched: false, via: "none" };
1508
- }
1509
-
1510
1510
  // src/registry/registry.ts
1511
1511
  var registry = /* @__PURE__ */ new Map();
1512
1512
  function defineActivityType(descriptor) {
@@ -1768,12 +1768,12 @@ var DictationDataSchema = DictationDataShape.check(
1768
1768
  })
1769
1769
  );
1770
1770
  var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1771
- function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void 0) {
1771
+ function captionFieldsOf(media, written2 = (captionsUrl) => captionsUrl !== void 0) {
1772
1772
  if (!isObject(media)) {
1773
1773
  return [];
1774
1774
  }
1775
1775
  const fields = [];
1776
- if (written(media.captionsUrl)) {
1776
+ if (written2(media.captionsUrl)) {
1777
1777
  fields.push("captionsUrl");
1778
1778
  }
1779
1779
  if (Array.isArray(media.tracks) && media.tracks.length > 0) {
@@ -1781,11 +1781,11 @@ function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void
1781
1781
  }
1782
1782
  return fields;
1783
1783
  }
1784
- function groupCaptionsField(group, written) {
1784
+ function groupCaptionsField(group, written2) {
1785
1785
  if (!isObject(group) || !isObject(group.stimulus) || !Array.isArray(group.items)) {
1786
1786
  return void 0;
1787
1787
  }
1788
- const [field] = captionFieldsOf(group.stimulus.media, written);
1788
+ const [field] = captionFieldsOf(group.stimulus.media, written2);
1789
1789
  if (field === void 0) {
1790
1790
  return void 0;
1791
1791
  }
@@ -2562,6 +2562,499 @@ var FillInTheBlanksDataSchema = z7.looseObject({
2562
2562
  }
2563
2563
  );
2564
2564
 
2565
+ // src/answer-leak.ts
2566
+ function placeholderOrder(passage) {
2567
+ return [...passage.matchAll(PLACEHOLDER_RE)].map((match) => match[1]);
2568
+ }
2569
+ function numberedPassage(passage) {
2570
+ let position = 0;
2571
+ return passage.replace(PLACEHOLDER_RE, () => {
2572
+ position += 1;
2573
+ return `[${position}]`;
2574
+ });
2575
+ }
2576
+ function passageWithoutPlaceholders(passage) {
2577
+ return passage.replace(PLACEHOLDER_RE, " ");
2578
+ }
2579
+ function fold(text) {
2580
+ const folded2 = text.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
2581
+ return folded2 === "" ? [] : folded2.split(" ");
2582
+ }
2583
+ function containsWords(haystack, needle) {
2584
+ if (needle.length === 0 || needle.length > haystack.length) {
2585
+ return false;
2586
+ }
2587
+ for (let start = 0; start + needle.length <= haystack.length; start += 1) {
2588
+ let match = true;
2589
+ for (let offset = 0; offset < needle.length; offset += 1) {
2590
+ if (haystack[start + offset] !== needle[offset]) {
2591
+ match = false;
2592
+ break;
2593
+ }
2594
+ }
2595
+ if (match) {
2596
+ return true;
2597
+ }
2598
+ }
2599
+ return false;
2600
+ }
2601
+ var isShort = (words) => words.length === 1 && [...words[0]].length < 4;
2602
+ function neighbours(passage, position) {
2603
+ const marker = `[${position}]`;
2604
+ const at = passage.indexOf(marker);
2605
+ if (at === -1) {
2606
+ return {};
2607
+ }
2608
+ const strip = (text) => text.replace(/\[\d+\]/g, " ");
2609
+ const left = fold(strip(passage.slice(0, at)));
2610
+ const right = fold(strip(passage.slice(at + marker.length)));
2611
+ const before = left[left.length - 1];
2612
+ const after = right[0];
2613
+ return {
2614
+ ...before !== void 0 ? { before } : {},
2615
+ ...after !== void 0 ? { after } : {}
2616
+ };
2617
+ }
2618
+ function revealsAnswer(text, answer, passage, position) {
2619
+ const words = fold(text);
2620
+ const needle = fold(answer);
2621
+ if (needle.length === 0) {
2622
+ return false;
2623
+ }
2624
+ if (!isShort(needle) || passage === void 0 || position === void 0) {
2625
+ return containsWords(words, needle);
2626
+ }
2627
+ const { before, after } = neighbours(passage, position);
2628
+ if (before === void 0 && after === void 0) {
2629
+ return containsWords(words, needle);
2630
+ }
2631
+ return before !== void 0 && containsWords(words, [before, ...needle]) || after !== void 0 && containsWords(words, [...needle, after]);
2632
+ }
2633
+
2634
+ // src/scoring/strategies/all-or-nothing.ts
2635
+ function allOrNothingStrategy(correctItems) {
2636
+ return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
2637
+ }
2638
+
2639
+ // src/scoring/strategies/partial.ts
2640
+ function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
2641
+ const reward = correctSelected / totalCorrect;
2642
+ const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
2643
+ return Math.max(0, reward - penalty);
2644
+ }
2645
+ function partialBlankStrategy(correctBlanks, totalBlanks) {
2646
+ return correctBlanks / totalBlanks;
2647
+ }
2648
+
2649
+ // src/scoring/activity-scorers/fill-in-the-blanks.ts
2650
+ function policyFor(blank) {
2651
+ return {
2652
+ ...blank.caseSensitive !== void 0 ? { caseSensitive: blank.caseSensitive } : {},
2653
+ ...blank.trimWhitespace !== void 0 ? { trim: blank.trimWhitespace } : {},
2654
+ ...blank.match
2655
+ };
2656
+ }
2657
+ function scoreFillInTheBlanks(data, response) {
2658
+ const details = [];
2659
+ const perBlankCorrect = [];
2660
+ for (const blank of data.blanks) {
2661
+ const rawInput = response.answers[blank.id];
2662
+ const input = typeof rawInput === "string" ? rawInput : "";
2663
+ const matched = matchText(input, blank.acceptedAnswers, policyFor(blank)).matched;
2664
+ perBlankCorrect.push(matched);
2665
+ details.push({
2666
+ itemId: blank.id,
2667
+ outcome: matched ? "correct" : "incorrect",
2668
+ learnerResponse: [input],
2669
+ correctResponse: [...blank.acceptedAnswers],
2670
+ weight: 1
2671
+ });
2672
+ }
2673
+ const correctBlanks = perBlankCorrect.filter(Boolean).length;
2674
+ const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
2675
+ return { score: scoreValue, maxScore: 1, feedback: null, details };
2676
+ }
2677
+
2678
+ // src/authoring/findings.ts
2679
+ var ITEM_FINDING_SEVERITY = {
2680
+ // multiple-choice
2681
+ mc_title_reveals_answer: "warning",
2682
+ mc_options_duplicate: "warning",
2683
+ mc_key_longest: "warning",
2684
+ mc_every_option_correct: "warning",
2685
+ mc_above_option_shuffled: "warning",
2686
+ mc_above_option: "advice",
2687
+ // fill-in-the-blanks
2688
+ fib_title_reveals_answer: "warning",
2689
+ fib_hint_reveals_answer: "warning",
2690
+ fib_answer_in_passage: "warning",
2691
+ fib_accepted_answer_redundant: "advice",
2692
+ // gap-select
2693
+ gs_title_reveals_answer: "warning",
2694
+ gs_choices_duplicate: "warning",
2695
+ gs_bank_no_distractor: "warning",
2696
+ gs_answer_in_passage: "warning",
2697
+ // written-response
2698
+ wr_criterion_name_duplicate: "warning",
2699
+ // read-aloud
2700
+ ra_text_long_for_time: "warning",
2701
+ // a set of items: a quiz, or an item group's items
2702
+ set_key_position_same: "warning"
2703
+ };
2704
+ function finding(code, path, message) {
2705
+ return { path: path.map(String), message, code, severity: ITEM_FINDING_SEVERITY[code] };
2706
+ }
2707
+ function normaliseFinding(found) {
2708
+ const documented = Object.hasOwn(ITEM_FINDING_SEVERITY, found.code) ? ITEM_FINDING_SEVERITY[found.code] : void 0;
2709
+ return {
2710
+ ...found,
2711
+ path: found.path.map(String),
2712
+ severity: documented ?? (found.severity === "advice" ? "advice" : "warning")
2713
+ };
2714
+ }
2715
+
2716
+ // src/authoring/critique.ts
2717
+ var written = (value) => typeof value === "string" && !isUnwritten(value) ? value : void 0;
2718
+ var folded = (text) => fold(text).join(" ");
2719
+ var listOf = (value) => Array.isArray(value) ? value : [];
2720
+ var lengthOf = (text) => [...text.trim()].length;
2721
+ var ordinal = (index) => index + 1;
2722
+ function duplicates(entries, textOf) {
2723
+ const seen = /* @__PURE__ */ new Map();
2724
+ const pairs = [];
2725
+ entries.forEach((entry, index) => {
2726
+ const text = textOf(entry);
2727
+ if (text === void 0) {
2728
+ return;
2729
+ }
2730
+ const key = folded(text);
2731
+ if (key === "") {
2732
+ return;
2733
+ }
2734
+ const first = seen.get(key);
2735
+ if (first === void 0) {
2736
+ seen.set(key, index);
2737
+ } else {
2738
+ pairs.push({ first, second: index });
2739
+ }
2740
+ });
2741
+ return pairs;
2742
+ }
2743
+ function printedIn(passage, answer) {
2744
+ const needle = fold(answer);
2745
+ return needle.length > 0 && !isShort(needle) && containsWords(fold(passageWithoutPlaceholders(passage)), needle);
2746
+ }
2747
+ var POSITIONAL = /(^| )(the above|las anteriores|los anteriores|as anteriores|os anteriores|[a-h] (and|or|y|o|e|ou) [a-h])( |$)/;
2748
+ var AGGREGATE = /(^| )(all of the above|none of the above|todas las anteriores|ninguna de las anteriores|todas as anteriores|nenhuma das anteriores)( |$)/;
2749
+ var KEY_LONGEST_RATIO = 1.5;
2750
+ var KEY_LONGEST_MARGIN = 10;
2751
+ function critiqueMultipleChoice(draft) {
2752
+ const findings = [];
2753
+ const options = listOf(draft.options);
2754
+ const texts = options.map((option) => isRecord(option) ? written(option.text) : void 0);
2755
+ const correct = options.map((option) => isRecord(option) && option.isCorrect === true);
2756
+ const hasMedia = options.map((option) => isRecord(option) && option.media !== void 0);
2757
+ const title = written(draft.title);
2758
+ const revealed = options.findIndex(
2759
+ (_, index) => correct[index] === true && title !== void 0 && texts[index] !== void 0 && revealsAnswer(title, texts[index])
2760
+ );
2761
+ if (revealed !== -1) {
2762
+ findings.push(
2763
+ finding(
2764
+ "mc_title_reveals_answer",
2765
+ ["title"],
2766
+ `The title, which the learner sees, contains the right answer ("${texts[revealed]}").`
2767
+ )
2768
+ );
2769
+ }
2770
+ for (const { first, second } of duplicates(
2771
+ options,
2772
+ (option) => isRecord(option) && option.media === void 0 ? written(option.text) : void 0
2773
+ )) {
2774
+ findings.push(
2775
+ finding(
2776
+ "mc_options_duplicate",
2777
+ ["options", second, "text"],
2778
+ `Options ${ordinal(first)} and ${ordinal(second)} read the same, so a learner cannot tell them apart.`
2779
+ )
2780
+ );
2781
+ }
2782
+ const keys = correct.flatMap((isKey, index) => isKey ? [index] : []);
2783
+ if (draft.mode === "single" && options.length >= 3 && keys.length === 1 && texts.every((text) => text !== void 0) && !hasMedia.some(Boolean)) {
2784
+ const key = keys[0];
2785
+ const keyLength = lengthOf(texts[key]);
2786
+ const longestOther = Math.max(
2787
+ ...texts.flatMap((text, index) => index === key ? [] : [lengthOf(text)])
2788
+ );
2789
+ if (keyLength >= longestOther * KEY_LONGEST_RATIO && keyLength - longestOther >= KEY_LONGEST_MARGIN) {
2790
+ findings.push(
2791
+ finding(
2792
+ "mc_key_longest",
2793
+ ["options", key, "text"],
2794
+ "The right option is much longer than every other, and a test-wise learner picks the longest. Bring the options to a similar length."
2795
+ )
2796
+ );
2797
+ }
2798
+ }
2799
+ if (draft.mode === "multi" && options.length >= 2 && correct.every(Boolean)) {
2800
+ findings.push(
2801
+ finding(
2802
+ "mc_every_option_correct",
2803
+ ["options"],
2804
+ "Every option is correct, so a learner who selects them all scores full marks without reading them."
2805
+ )
2806
+ );
2807
+ }
2808
+ texts.forEach((text, index) => {
2809
+ if (text === void 0) {
2810
+ return;
2811
+ }
2812
+ const words = folded(text);
2813
+ if (draft.shuffle === true && POSITIONAL.test(words)) {
2814
+ findings.push(
2815
+ finding(
2816
+ "mc_above_option_shuffled",
2817
+ ["options", index, "text"],
2818
+ `Option ${ordinal(index)} points at other options by position, and this question shuffles them: after shuffling it points at the wrong ones.`
2819
+ )
2820
+ );
2821
+ } else if (AGGREGATE.test(words)) {
2822
+ findings.push(
2823
+ finding(
2824
+ "mc_above_option",
2825
+ ["options", index, "text"],
2826
+ `Option ${ordinal(index)} is "all" or "none of the above". A learner who spots two right options can choose "all" without knowing the rest; "none" tests only what is wrong.`
2827
+ )
2828
+ );
2829
+ }
2830
+ });
2831
+ return findings;
2832
+ }
2833
+ function redundantUnder(blank, earlier, later) {
2834
+ try {
2835
+ const { levenshtein: _typos, ...policy } = policyFor(blank);
2836
+ return matchText(later, earlier, policy).matched;
2837
+ } catch {
2838
+ return false;
2839
+ }
2840
+ }
2841
+ function critiqueFillInTheBlanks(draft) {
2842
+ const findings = [];
2843
+ const passage = typeof draft.passage === "string" ? draft.passage : "";
2844
+ const numbered = numberedPassage(passage);
2845
+ const order = placeholderOrder(passage);
2846
+ const blanks = listOf(draft.blanks);
2847
+ const answersOf = (blank) => isRecord(blank) ? listOf(blank.acceptedAnswers).flatMap((answer) => written(answer) ?? []) : [];
2848
+ const positionOf = (blank) => {
2849
+ const at = isRecord(blank) && typeof blank.id === "string" ? order.indexOf(blank.id) : -1;
2850
+ return at === -1 ? void 0 : at + 1;
2851
+ };
2852
+ const title = written(draft.title);
2853
+ if (title !== void 0 && blanks.some(
2854
+ (blank) => answersOf(blank).some((answer) => revealsAnswer(title, answer, numbered, positionOf(blank)))
2855
+ )) {
2856
+ findings.push(
2857
+ finding(
2858
+ "fib_title_reveals_answer",
2859
+ ["title"],
2860
+ "The title, which the learner sees, contains an answer to a blank."
2861
+ )
2862
+ );
2863
+ }
2864
+ blanks.forEach((blank, index) => {
2865
+ if (!isRecord(blank)) {
2866
+ return;
2867
+ }
2868
+ const answers = answersOf(blank);
2869
+ const hint = written(blank.hint);
2870
+ if (hint !== void 0 && answers.some((answer) => revealsAnswer(hint, answer, numbered, positionOf(blank)))) {
2871
+ findings.push(
2872
+ finding(
2873
+ "fib_hint_reveals_answer",
2874
+ ["blanks", index, "hint"],
2875
+ `The hint for blank ${ordinal(index)} contains its answer.`
2876
+ )
2877
+ );
2878
+ }
2879
+ const accepted = listOf(blank.acceptedAnswers);
2880
+ const printed = accepted.findIndex((answer) => {
2881
+ const text = written(answer);
2882
+ return text !== void 0 && printedIn(passage, text);
2883
+ });
2884
+ if (printed !== -1) {
2885
+ findings.push(
2886
+ finding(
2887
+ "fib_answer_in_passage",
2888
+ ["blanks", index, "acceptedAnswers", printed],
2889
+ `The passage prints "${accepted[printed]}", an answer to blank ${ordinal(index)}, where the learner can copy it.`
2890
+ )
2891
+ );
2892
+ }
2893
+ accepted.forEach((later, laterIndex) => {
2894
+ const laterText = written(later);
2895
+ if (laterText === void 0) {
2896
+ return;
2897
+ }
2898
+ const earlier = accepted.slice(0, laterIndex).findIndex((candidate) => {
2899
+ const text = written(candidate);
2900
+ return text !== void 0 && redundantUnder(blank, text, laterText);
2901
+ });
2902
+ if (earlier !== -1) {
2903
+ findings.push(
2904
+ finding(
2905
+ "fib_accepted_answer_redundant",
2906
+ ["blanks", index, "acceptedAnswers", laterIndex],
2907
+ `"${laterText}" is already accepted as "${accepted[earlier]}" under this blank's matching.`
2908
+ )
2909
+ );
2910
+ }
2911
+ });
2912
+ });
2913
+ return findings;
2914
+ }
2915
+ function critiqueGapSelect(draft) {
2916
+ const findings = [];
2917
+ const passage = typeof draft.passage === "string" ? draft.passage : "";
2918
+ const numbered = numberedPassage(passage);
2919
+ const order = placeholderOrder(passage);
2920
+ const gaps = listOf(draft.gaps);
2921
+ const banks = listOf(draft.banks);
2922
+ const choiceText = (choice) => isRecord(choice) ? written(choice.text) : void 0;
2923
+ const choicesOf = (gap) => {
2924
+ if (Array.isArray(gap.choices)) {
2925
+ return gap.choices;
2926
+ }
2927
+ const bank = banks.find((candidate) => isRecord(candidate) && candidate.id === gap.bankId);
2928
+ return isRecord(bank) ? listOf(bank.choices) : [];
2929
+ };
2930
+ const answerOf = (gap) => choiceText(
2931
+ choicesOf(gap).find((choice) => isRecord(choice) && choice.id === gap.correctChoiceId)
2932
+ );
2933
+ const positionOf = (gap) => {
2934
+ const at = typeof gap.id === "string" ? order.indexOf(gap.id) : -1;
2935
+ return at === -1 ? void 0 : at + 1;
2936
+ };
2937
+ const title = written(draft.title);
2938
+ if (title !== void 0 && gaps.some((gap) => {
2939
+ if (!isRecord(gap)) {
2940
+ return false;
2941
+ }
2942
+ const answer = answerOf(gap);
2943
+ return answer !== void 0 && revealsAnswer(title, answer, numbered, positionOf(gap));
2944
+ })) {
2945
+ findings.push(
2946
+ finding(
2947
+ "gs_title_reveals_answer",
2948
+ ["title"],
2949
+ "The title, which the learner sees, contains the answer to a gap."
2950
+ )
2951
+ );
2952
+ }
2953
+ const reportDuplicates = (choices, path) => {
2954
+ for (const { first, second } of duplicates(choices, choiceText)) {
2955
+ findings.push(
2956
+ finding(
2957
+ "gs_choices_duplicate",
2958
+ [...path, "choices", second, "text"],
2959
+ `Choices ${ordinal(first)} and ${ordinal(second)} read the same, so a learner cannot tell them apart.`
2960
+ )
2961
+ );
2962
+ }
2963
+ };
2964
+ gaps.forEach((gap, index) => {
2965
+ if (!isRecord(gap)) {
2966
+ return;
2967
+ }
2968
+ if (Array.isArray(gap.choices)) {
2969
+ reportDuplicates(gap.choices, ["gaps", index]);
2970
+ }
2971
+ const answer = answerOf(gap);
2972
+ if (answer !== void 0 && printedIn(passage, answer)) {
2973
+ findings.push(
2974
+ finding(
2975
+ "gs_answer_in_passage",
2976
+ ["gaps", index, "correctChoiceId"],
2977
+ `The passage prints "${answer}", the answer to gap ${ordinal(index)}.`
2978
+ )
2979
+ );
2980
+ }
2981
+ });
2982
+ banks.forEach((bank, index) => {
2983
+ if (!isRecord(bank)) {
2984
+ return;
2985
+ }
2986
+ const choices = listOf(bank.choices);
2987
+ reportDuplicates(choices, ["banks", index]);
2988
+ const drawnOn = gaps.filter((gap) => isRecord(gap) && gap.bankId === bank.id).length;
2989
+ if (drawnOn >= 2 && choices.length <= drawnOn) {
2990
+ findings.push(
2991
+ finding(
2992
+ "gs_bank_no_distractor",
2993
+ ["banks", index, "choices"],
2994
+ `This bank offers ${choices.length} choices to ${drawnOn} gaps, so the last gap is answered by elimination. Add a choice that answers no gap.`
2995
+ )
2996
+ );
2997
+ }
2998
+ });
2999
+ return findings;
3000
+ }
3001
+ function critiqueWrittenResponse(draft) {
3002
+ const rubric = isRecord(draft.rubric) ? draft.rubric : void 0;
3003
+ const criteria = rubric === void 0 ? [] : listOf(rubric.criteria);
3004
+ return duplicates(
3005
+ criteria,
3006
+ (criterion) => isRecord(criterion) ? written(criterion.name) : void 0
3007
+ ).map(
3008
+ ({ first, second }) => finding(
3009
+ "wr_criterion_name_duplicate",
3010
+ ["rubric", "criteria", second, "name"],
3011
+ `Criteria ${ordinal(first)} and ${ordinal(second)} have the same name. Feedback and grades name a criterion, so one of them cannot be told apart.`
3012
+ )
3013
+ );
3014
+ }
3015
+ var WORDS_PER_SECOND = 2.5;
3016
+ function critiqueReadAloud(draft) {
3017
+ const text = written(draft.referenceText);
3018
+ const recording = isRecord(draft.recording) ? draft.recording : void 0;
3019
+ const maxSeconds = recording?.maxSeconds;
3020
+ if (text === void 0 || typeof maxSeconds !== "number" || !(maxSeconds > 0)) {
3021
+ return [];
3022
+ }
3023
+ const words = countWords(text);
3024
+ if (words / maxSeconds <= WORDS_PER_SECOND) {
3025
+ return [];
3026
+ }
3027
+ return [
3028
+ finding(
3029
+ "ra_text_long_for_time",
3030
+ ["recording", "maxSeconds"],
3031
+ `${words} words in ${maxSeconds} seconds is faster than a fluent reader reads aloud (${WORDS_PER_SECOND} words a second). Allow at least ${Math.ceil(words / WORDS_PER_SECOND)} seconds, or shorten the text.`
3032
+ )
3033
+ ];
3034
+ }
3035
+ var KEY_POSITION_MINIMUM = 4;
3036
+ function keyPositionFinding(items, path) {
3037
+ const positions = items.flatMap((item) => {
3038
+ if (!isRecord(item) || item.type !== "multiple-choice" || item.mode !== "single" || item.shuffle === true) {
3039
+ return [];
3040
+ }
3041
+ const keys = listOf(item.options).flatMap(
3042
+ (option, index) => isRecord(option) && option.isCorrect === true ? [index] : []
3043
+ );
3044
+ return keys.length === 1 ? keys : [];
3045
+ });
3046
+ if (positions.length < KEY_POSITION_MINIMUM || !positions.every((at) => at === positions[0])) {
3047
+ return [];
3048
+ }
3049
+ return [
3050
+ finding(
3051
+ "set_key_position_same",
3052
+ path,
3053
+ `In all ${positions.length} single-answer questions the right option is option ${ordinal(positions[0])}. Vary its position, or shuffle the options.`
3054
+ )
3055
+ ];
3056
+ }
3057
+
2565
3058
  // src/authoring/fill-in-the-blanks.ts
2566
3059
  var fillInTheBlanksAuthoring = {
2567
3060
  createDraft: ({ newId }) => ({
@@ -2573,7 +3066,8 @@ var fillInTheBlanksAuthoring = {
2573
3066
  blanks: [],
2574
3067
  scoringStrategy: "all-or-nothing"
2575
3068
  }),
2576
- checkDraft: checkFillInTheBlanksDraft
3069
+ checkDraft: checkFillInTheBlanksDraft,
3070
+ critique: critiqueFillInTheBlanks
2577
3071
  };
2578
3072
  function checkFillInTheBlanksDraft(draft) {
2579
3073
  const issues = checkIdentity(draft, "fill-in-the-blanks");
@@ -2767,7 +3261,8 @@ var gapSelectAuthoring = {
2767
3261
  gaps: [],
2768
3262
  scoringStrategy: "all-or-nothing"
2769
3263
  }),
2770
- checkDraft: checkGapSelectDraft
3264
+ checkDraft: checkGapSelectDraft,
3265
+ critique: critiqueGapSelect
2771
3266
  };
2772
3267
  function checkGapSelectDraft(draft) {
2773
3268
  const issues = checkIdentity(draft, "gap-select");
@@ -2846,10 +3341,10 @@ function checkGaps(gaps, banks, bankIds, gapIds) {
2846
3341
  if (!isRecord(gap)) {
2847
3342
  continue;
2848
3343
  }
2849
- const ordinal = index + 1;
2850
- const name = typeof gap.id === "string" && gap.id !== "" ? `"${gap.id}"` : ordinal;
3344
+ const ordinal2 = index + 1;
3345
+ const name = typeof gap.id === "string" && gap.id !== "" ? `"${gap.id}"` : ordinal2;
2851
3346
  if (isMissingId(gap.id)) {
2852
- issues.push(issue("gs_gap_id_required", ["gaps", index, "id"], `Gap ${ordinal} has no id.`));
3347
+ issues.push(issue("gs_gap_id_required", ["gaps", index, "id"], `Gap ${ordinal2} has no id.`));
2853
3348
  } else if (typeof gap.id === "string") {
2854
3349
  if (seen.has(gap.id)) {
2855
3350
  repeated.add(gap.id);
@@ -3067,7 +3562,8 @@ var multipleChoiceAuthoring = {
3067
3562
  { id: newId(), text: "", isCorrect: false }
3068
3563
  ]
3069
3564
  }),
3070
- checkDraft: checkMultipleChoiceDraft
3565
+ checkDraft: checkMultipleChoiceDraft,
3566
+ critique: critiqueMultipleChoice
3071
3567
  };
3072
3568
  function checkMultipleChoiceDraft(draft) {
3073
3569
  const issues = checkIdentity(draft, "multiple-choice");
@@ -3097,10 +3593,10 @@ function checkMultipleChoiceDraft(draft) {
3097
3593
  if (!isRecord(option)) {
3098
3594
  continue;
3099
3595
  }
3100
- const ordinal = index + 1;
3596
+ const ordinal2 = index + 1;
3101
3597
  if (isMissingId(option.id)) {
3102
3598
  issues.push(
3103
- issue("mc_option_id_required", ["options", index, "id"], `Option ${ordinal} has no id.`)
3599
+ issue("mc_option_id_required", ["options", index, "id"], `Option ${ordinal2} has no id.`)
3104
3600
  );
3105
3601
  } else if (typeof option.id === "string") {
3106
3602
  if (seen.has(option.id)) {
@@ -3113,7 +3609,7 @@ function checkMultipleChoiceDraft(draft) {
3113
3609
  issue(
3114
3610
  "mc_option_text_required",
3115
3611
  ["options", index, "text"],
3116
- `Write the text of option ${ordinal}.`
3612
+ `Write the text of option ${ordinal2}.`
3117
3613
  )
3118
3614
  );
3119
3615
  }
@@ -3122,7 +3618,7 @@ function checkMultipleChoiceDraft(draft) {
3122
3618
  issue(
3123
3619
  "mc_option_correctness_required",
3124
3620
  ["options", index, "isCorrect"],
3125
- `Say whether option ${ordinal} is correct.`
3621
+ `Say whether option ${ordinal2} is correct.`
3126
3622
  )
3127
3623
  );
3128
3624
  }
@@ -3130,7 +3626,7 @@ function checkMultipleChoiceDraft(draft) {
3130
3626
  correct += 1;
3131
3627
  }
3132
3628
  if (isRecord(option.media)) {
3133
- issues.push(...checkOptionMedia(option.media, index, ordinal));
3629
+ issues.push(...checkOptionMedia(option.media, index, ordinal2));
3134
3630
  }
3135
3631
  }
3136
3632
  for (const id of repeated) {
@@ -3159,7 +3655,7 @@ function checkMultipleChoiceDraft(draft) {
3159
3655
  issues.push(...checkSharedOptional(draft));
3160
3656
  return issues;
3161
3657
  }
3162
- function checkOptionMedia(media, index, ordinal) {
3658
+ function checkOptionMedia(media, index, ordinal2) {
3163
3659
  const path = ["options", index, "media"];
3164
3660
  const kind = media.type;
3165
3661
  if (kind === "video" || kind === "embed") {
@@ -3167,7 +3663,7 @@ function checkOptionMedia(media, index, ordinal) {
3167
3663
  issue(
3168
3664
  "mc_option_media_kind",
3169
3665
  [...path, "type"],
3170
- `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.`
3666
+ `Option ${ordinal2} 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.`
3171
3667
  )
3172
3668
  ];
3173
3669
  }
@@ -3335,7 +3831,8 @@ var readAloudAuthoring = {
3335
3831
  recording: { maxSeconds: 0 },
3336
3832
  scoring: { dimensions: [] }
3337
3833
  }),
3338
- checkDraft: checkReadAloudDraft
3834
+ checkDraft: checkReadAloudDraft,
3835
+ critique: critiqueReadAloud
3339
3836
  };
3340
3837
  function checkReadAloudDraft(draft) {
3341
3838
  const issues = checkIdentity(draft, "read-aloud");
@@ -3611,7 +4108,8 @@ var writtenResponseAuthoring = {
3611
4108
  minWords: 0,
3612
4109
  maxWords: 0
3613
4110
  }),
3614
- checkDraft: checkWrittenResponseDraft
4111
+ checkDraft: checkWrittenResponseDraft,
4112
+ critique: critiqueWrittenResponse
3615
4113
  };
3616
4114
  function checkWrittenResponseDraft(draft) {
3617
4115
  const issues = checkIdentity(draft, "written-response");
@@ -3691,11 +4189,11 @@ function checkCriteria(criteria) {
3691
4189
  everyWeightUsable = false;
3692
4190
  continue;
3693
4191
  }
3694
- const ordinal = index + 1;
4192
+ const ordinal2 = index + 1;
3695
4193
  const path = ["rubric", "criteria", index];
3696
4194
  if (isUnwritten(criterion.name)) {
3697
4195
  issues.push(
3698
- issue("wr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`)
4196
+ issue("wr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal2}.`)
3699
4197
  );
3700
4198
  }
3701
4199
  const weight = criterion.weight;
@@ -3705,7 +4203,7 @@ function checkCriteria(criteria) {
3705
4203
  issue(
3706
4204
  "wr_criterion_weight_required",
3707
4205
  [...path, "weight"],
3708
- `Give rubric criterion ${ordinal} a weight.`
4206
+ `Give rubric criterion ${ordinal2} a weight.`
3709
4207
  )
3710
4208
  );
3711
4209
  } else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
@@ -4085,50 +4583,6 @@ function scoreDictation(data, response) {
4085
4583
  return { score: alignment.similarity, maxScore: 1, feedback: null, details };
4086
4584
  }
4087
4585
 
4088
- // src/scoring/strategies/all-or-nothing.ts
4089
- function allOrNothingStrategy(correctItems) {
4090
- return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
4091
- }
4092
-
4093
- // src/scoring/strategies/partial.ts
4094
- function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
4095
- const reward = correctSelected / totalCorrect;
4096
- const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
4097
- return Math.max(0, reward - penalty);
4098
- }
4099
- function partialBlankStrategy(correctBlanks, totalBlanks) {
4100
- return correctBlanks / totalBlanks;
4101
- }
4102
-
4103
- // src/scoring/activity-scorers/fill-in-the-blanks.ts
4104
- function policyFor(blank) {
4105
- return {
4106
- ...blank.caseSensitive !== void 0 ? { caseSensitive: blank.caseSensitive } : {},
4107
- ...blank.trimWhitespace !== void 0 ? { trim: blank.trimWhitespace } : {},
4108
- ...blank.match
4109
- };
4110
- }
4111
- function scoreFillInTheBlanks(data, response) {
4112
- const details = [];
4113
- const perBlankCorrect = [];
4114
- for (const blank of data.blanks) {
4115
- const rawInput = response.answers[blank.id];
4116
- const input = typeof rawInput === "string" ? rawInput : "";
4117
- const matched = matchText(input, blank.acceptedAnswers, policyFor(blank)).matched;
4118
- perBlankCorrect.push(matched);
4119
- details.push({
4120
- itemId: blank.id,
4121
- outcome: matched ? "correct" : "incorrect",
4122
- learnerResponse: [input],
4123
- correctResponse: [...blank.acceptedAnswers],
4124
- weight: 1
4125
- });
4126
- }
4127
- const correctBlanks = perBlankCorrect.filter(Boolean).length;
4128
- const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
4129
- return { score: scoreValue, maxScore: 1, feedback: null, details };
4130
- }
4131
-
4132
4586
  // src/scoring/activity-scorers/gap-select.ts
4133
4587
  function choicesFor(data, gap) {
4134
4588
  if (gap.choices !== void 0) {
@@ -4532,8 +4986,12 @@ export {
4532
4986
  maxUnits,
4533
4987
  MediaSchema,
4534
4988
  RedactedMediaSchema,
4535
- PLACEHOLDER_RE,
4536
4989
  FillInTheBlanksDataSchema,
4990
+ placeholderOrder,
4991
+ numberedPassage,
4992
+ fold,
4993
+ containsWords,
4994
+ revealsAnswer,
4537
4995
  withValidationScope,
4538
4996
  DICTATION_MAX_TRANSCRIPT_LENGTH,
4539
4997
  DICTATION_MAX_TEXT_LENGTH,
@@ -4564,6 +5022,11 @@ export {
4564
5022
  covers,
4565
5023
  pathKey,
4566
5024
  coveredPathsOf,
5025
+ countWords,
5026
+ levenshteinDistance,
5027
+ matchText,
5028
+ normaliseFinding,
5029
+ keyPositionFinding,
4567
5030
  MultipleChoiceOptionMediaSchema,
4568
5031
  MultipleChoiceDataSchema,
4569
5032
  READ_ALOUD_MAX_REFERENCE_LENGTH,
@@ -4574,14 +5037,11 @@ export {
4574
5037
  SPEECH_ASSESSMENT_MAX_TEXT_LENGTH,
4575
5038
  CANONICAL_LOCALE_RE,
4576
5039
  ReadAloudDataSchema,
4577
- countWords,
4578
5040
  GapSelectDataSchema,
4579
5041
  isOwnSchema,
4580
5042
  readSchema,
4581
5043
  standardJsonSchemaOf,
4582
5044
  WrittenResponseDataSchema,
4583
- levenshteinDistance,
4584
- matchText,
4585
5045
  defineActivityType,
4586
5046
  registerActivityType,
4587
5047
  getActivityTypeDescriptor,
@@ -4594,4 +5054,4 @@ export {
4594
5054
  dictationType,
4595
5055
  readAloudType
4596
5056
  };
4597
- //# sourceMappingURL=chunk-FBKTION6.js.map
5057
+ //# sourceMappingURL=chunk-QS7B42ZR.js.map