@intellectif/lk-core 0.12.0 → 0.13.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 (51) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/dist/{activity-B2QxK15j.d.cts → activity-DfAmJ1sl.d.cts} +134 -7
  4. package/dist/{activity-B2QxK15j.d.ts → activity-DfAmJ1sl.d.ts} +134 -7
  5. package/dist/{chunk-VYE6BYON.cjs → chunk-DQKJUMUW.cjs} +38 -15
  6. package/dist/chunk-DQKJUMUW.cjs.map +1 -0
  7. package/dist/{chunk-RK2NVTHZ.js → chunk-FGN4LLSP.js} +50 -6
  8. package/dist/chunk-FGN4LLSP.js.map +1 -0
  9. package/dist/{chunk-O2IVKTHV.cjs → chunk-FT7SAC3X.cjs} +4 -4
  10. package/dist/{chunk-O2IVKTHV.cjs.map → chunk-FT7SAC3X.cjs.map} +1 -1
  11. package/dist/{chunk-UQPNJP2O.js → chunk-PYP3HCRQ.js} +2 -2
  12. package/dist/{chunk-4OHWLCI6.cjs → chunk-UHQVIC3P.cjs} +58 -14
  13. package/dist/chunk-UHQVIC3P.cjs.map +1 -0
  14. package/dist/{chunk-FVN37GIH.js → chunk-XKKKWKRH.js} +1944 -177
  15. package/dist/chunk-XKKKWKRH.js.map +1 -0
  16. package/dist/{chunk-J6POE4HL.cjs → chunk-XONKWL6D.cjs} +1785 -18
  17. package/dist/chunk-XONKWL6D.cjs.map +1 -0
  18. package/dist/{chunk-7JMMPE7O.js → chunk-Y36S3X3L.js} +26 -3
  19. package/dist/chunk-Y36S3X3L.js.map +1 -0
  20. package/dist/{index-B_scInLM.d.cts → index-Bz9wUigA.d.cts} +181 -4
  21. package/dist/{index-CRKtWP5G.d.ts → index-CJbbVE9o.d.ts} +181 -4
  22. package/dist/index.cjs +128 -73
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +14 -6
  25. package/dist/index.d.ts +14 -6
  26. package/dist/index.js +60 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/schemas.cjs +17 -3
  29. package/dist/schemas.cjs.map +1 -1
  30. package/dist/schemas.d.cts +2 -2
  31. package/dist/schemas.d.ts +2 -2
  32. package/dist/schemas.js +16 -2
  33. package/dist/scoring.cjs +19 -3
  34. package/dist/scoring.cjs.map +1 -1
  35. package/dist/scoring.d.cts +150 -5
  36. package/dist/scoring.d.ts +150 -5
  37. package/dist/scoring.js +18 -2
  38. package/dist/xapi.cjs +3 -3
  39. package/dist/xapi.d.cts +1 -1
  40. package/dist/xapi.d.ts +1 -1
  41. package/dist/xapi.js +2 -2
  42. package/package.json +1 -1
  43. package/vectors/README.md +9 -1
  44. package/vectors/scoring.json +12023 -1
  45. package/dist/chunk-4OHWLCI6.cjs.map +0 -1
  46. package/dist/chunk-7JMMPE7O.js.map +0 -1
  47. package/dist/chunk-FVN37GIH.js.map +0 -1
  48. package/dist/chunk-J6POE4HL.cjs.map +0 -1
  49. package/dist/chunk-RK2NVTHZ.js.map +0 -1
  50. package/dist/chunk-VYE6BYON.cjs.map +0 -1
  51. /package/dist/{chunk-UQPNJP2O.js.map → chunk-PYP3HCRQ.js.map} +0 -0
@@ -38,6 +38,876 @@ var DeferredScoringError = class extends Error {
38
38
  activityType;
39
39
  };
40
40
 
41
+ // src/validation-scope.ts
42
+ var active = null;
43
+ function withValidationScope(run) {
44
+ if (active !== null) {
45
+ return run();
46
+ }
47
+ active = /* @__PURE__ */ new Map();
48
+ try {
49
+ return run();
50
+ } finally {
51
+ active = null;
52
+ }
53
+ }
54
+ function scopeCache(key, create) {
55
+ if (active === null) {
56
+ return void 0;
57
+ }
58
+ let value = active.get(key);
59
+ if (value === void 0) {
60
+ value = create();
61
+ active.set(key, value);
62
+ }
63
+ return value;
64
+ }
65
+
66
+ // src/scoring/dictation/normalize.ts
67
+ var DICTATION_MAX_TRANSCRIPT_LENGTH = 2e3;
68
+ var DICTATION_MAX_TEXT_LENGTH = 8e3;
69
+ var DICTATION_MAX_EQUIVALENCE_LENGTH = 200;
70
+ var DICTATION_MAX_EQUIVALENCES = 100;
71
+ var DICTATION_MAX_ACCEPTED_TRANSCRIPTS = 10;
72
+ var WORKING_LENGTH = 2 * DICTATION_MAX_TEXT_LENGTH;
73
+ var UNSPACED_REVEAL_MIN_CHARACTERS = 4;
74
+ var cp = (...points) => String.fromCodePoint(...points);
75
+ var NEXT_LINE = cp(133);
76
+ var MIDDLE_DOT = cp(183);
77
+ var TIBETAN_TSHEG = cp(3851);
78
+ var COMBINING_KEYCAP = cp(8419);
79
+ var WAVING_BLACK_FLAG = 127988;
80
+ var ZWNJ = 8204;
81
+ var ZWJ = 8205;
82
+ var MONGOLIAN_VOWEL_SEPARATOR = 6158;
83
+ var JOINERS = cp(ZWNJ, ZWJ, MONGOLIAN_VOWEL_SEPARATOR);
84
+ var MONGOLIAN_SELECTORS = cp(6155, 6156, 6157, 6159);
85
+ var TAGS = `${cp(917536)}-${cp(917631)}`;
86
+ var STRETCHERS = cp(1600, 2042);
87
+ var COMPATIBILITY_RE = new RegExp(
88
+ `[${[
89
+ cp(170),
90
+ cp(178, 179, 181, 185, 186),
91
+ `${cp(188)}-${cp(190)}`,
92
+ `${cp(306)}-${cp(307)}`,
93
+ `${cp(319)}-${cp(320)}`,
94
+ cp(329),
95
+ `${cp(452)}-${cp(460)}`,
96
+ `${cp(497)}-${cp(499)}`,
97
+ `${cp(8304)}-${cp(8351)}`,
98
+ `${cp(8448)}-${cp(8591)}`,
99
+ `${cp(9312)}-${cp(9471)}`,
100
+ `${cp(11904)}-${cp(12255)}`,
101
+ `${cp(12881)}-${cp(12895)}`,
102
+ `${cp(12977)}-${cp(12991)}`,
103
+ `${cp(64256)}-${cp(65023)}`,
104
+ `${cp(65040)}-${cp(65049)}`,
105
+ `${cp(65072)}-${cp(65135)}`,
106
+ `${cp(65136)}-${cp(65278)}`,
107
+ `${cp(65281)}-${cp(65373)}`,
108
+ `${cp(65375)}-${cp(65506)}`,
109
+ `${cp(65508)}-${cp(65518)}`
110
+ ].join("")}]`,
111
+ "gu"
112
+ );
113
+ var COMPATIBILITY_JAMO = new Map(
114
+ Array.from({ length: 12686 - 12593 + 1 }, (_, index) => {
115
+ const jamo = cp(12593 + index);
116
+ return [jamo.normalize("NFKC"), jamo];
117
+ })
118
+ );
119
+ var SEQUENCE_FOLDS = /* @__PURE__ */ new Map([
120
+ [cp(3363, 3405, ZWJ), cp(3450)],
121
+ [cp(3368, 3405, ZWJ), cp(3451)],
122
+ [cp(3376, 3405, ZWJ), cp(3452)],
123
+ [cp(3378, 3405, ZWJ), cp(3453)],
124
+ [cp(3379, 3405, ZWJ), cp(3454)],
125
+ [cp(3349, 3405, ZWJ), cp(3455)],
126
+ [cp(3374, 3405, ZWJ), cp(3412)],
127
+ [cp(3375, 3405, ZWJ), cp(3413)],
128
+ [cp(3380, 3405, ZWJ), cp(3414)],
129
+ [cp(2468, 2509, ZWJ), cp(2510)],
130
+ [cp(3368, 3405, 3377), cp(3451, 3405, 3377)],
131
+ [cp(2352, 2381, ZWJ), cp(2353, 2381)],
132
+ [cp(2353, 2381, ZWJ), cp(2353, 2381)]
133
+ ]);
134
+ var SEQUENCE_FOLDS_RE = new RegExp([...SEQUENCE_FOLDS.keys()].join("|"), "gu");
135
+ var THAI_SARA_AM_PARTS_RE = new RegExp(
136
+ `${cp(3661)}([${cp(3656)}-${cp(3659)}]*)${cp(3634)}`,
137
+ "gu"
138
+ );
139
+ var LAO_SARA_AM_PARTS_RE = new RegExp(
140
+ `${cp(3789)}([${cp(3784)}-${cp(3787)}]*)${cp(3762)}`,
141
+ "gu"
142
+ );
143
+ function foldSequences(text) {
144
+ return text.replace(SEQUENCE_FOLDS_RE, (sequence) => SEQUENCE_FOLDS.get(sequence)).replace(THAI_SARA_AM_PARTS_RE, `$1${cp(3635)}`).replace(LAO_SARA_AM_PARTS_RE, `$1${cp(3763)}`);
145
+ }
146
+ var FOLDED = new Map(
147
+ [
148
+ [8216, "'"],
149
+ // LEFT SINGLE QUOTATION MARK
150
+ [8217, "'"],
151
+ // RIGHT SINGLE QUOTATION MARK (the smart apostrophe)
152
+ [8218, "'"],
153
+ // SINGLE LOW-9 QUOTATION MARK
154
+ [8219, "'"],
155
+ // SINGLE HIGH-REVERSED-9 QUOTATION MARK
156
+ [8242, "'"],
157
+ // PRIME
158
+ [700, "'"],
159
+ // MODIFIER LETTER APOSTROPHE
160
+ [180, "'"],
161
+ // ACUTE ACCENT, used as an apostrophe
162
+ [96, "'"],
163
+ // GRAVE ACCENT, used as an apostrophe
164
+ [1523, "'"],
165
+ // HEBREW PUNCTUATION GERESH
166
+ [8220, '"'],
167
+ // LEFT DOUBLE QUOTATION MARK
168
+ [8221, '"'],
169
+ // RIGHT DOUBLE QUOTATION MARK
170
+ [8222, '"'],
171
+ // DOUBLE LOW-9 QUOTATION MARK
172
+ [8243, '"'],
173
+ // DOUBLE PRIME
174
+ [1524, '"'],
175
+ // HEBREW PUNCTUATION GERSHAYIM
176
+ [8208, "-"],
177
+ // HYPHEN
178
+ [8209, "-"],
179
+ // NON-BREAKING HYPHEN
180
+ [1470, "-"],
181
+ // HEBREW PUNCTUATION MAQAF
182
+ [1418, "-"],
183
+ // ARMENIAN HYPHEN
184
+ [3852, TIBETAN_TSHEG],
185
+ // TIBETAN MARK DELIMITER TSHEG BSTAR
186
+ [65374, cp(12316)]
187
+ // FULLWIDTH TILDE, which a Japanese keyboard types for the WAVE DASH
188
+ ].map(([point, folded]) => [cp(point), folded])
189
+ );
190
+ var FOLDED_RE = new RegExp(`[${[...FOLDED.keys()].join("")}]`, "gu");
191
+ var DRAWN_FORMAT_CHARACTERS = `${cp(1536)}-${cp(1541)}${cp(1757, 1807, 2192, 2193, 2274, 69821, 69837)}`;
192
+ var INVISIBLE_RE = new RegExp(
193
+ `(?![\\t\\n\\v\\f\\r${NEXT_LINE}${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}${DRAWN_FORMAT_CHARACTERS}])[\\p{Cc}\\p{Cf}\\p{Default_Ignorable_Code_Point}${STRETCHERS}]`,
194
+ "gu"
195
+ );
196
+ var REPEATED_JOINER_RE = new RegExp(`([${JOINERS}])\\1+`, "gu");
197
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
198
+ var REPLACEMENT_CHARACTER = cp(65533);
199
+ var CONTEXTUAL_RE = new RegExp(`[${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}]`, "u");
200
+ var WHITESPACE_RE = new RegExp(`[\\s${NEXT_LINE}]+`, "gu");
201
+ var UNSPACED_CLASS = "\\p{Script_Extensions=Han}\\p{Script_Extensions=Hiragana}\\p{Script_Extensions=Katakana}\\p{Script_Extensions=Thai}\\p{Script_Extensions=Lao}\\p{Script_Extensions=Khmer}\\p{Script_Extensions=Myanmar}";
202
+ var UNSPACED_CHARACTER = `(?!\\p{Nd})[${UNSPACED_CLASS}]`;
203
+ var IN_WORD_PUNCTUATION = `'\\-${MIDDLE_DOT}${TIBETAN_TSHEG}`;
204
+ var PUNCTUATION_RE = new RegExp(
205
+ [
206
+ `[${IN_WORD_PUNCTUATION}](?<![\\p{L}\\p{M}\\p{N}][${IN_WORD_PUNCTUATION}])`,
207
+ `[${IN_WORD_PUNCTUATION}](?![\\p{L}\\p{M}\\p{N}])`,
208
+ `${MIDDLE_DOT}(?<=${UNSPACED_CHARACTER}${MIDDLE_DOT})`,
209
+ `${MIDDLE_DOT}(?=${UNSPACED_CHARACTER})`,
210
+ `(?![#*]${COMBINING_KEYCAP})[^\\P{P}${IN_WORD_PUNCTUATION}]`
211
+ ].join("|"),
212
+ "gu"
213
+ );
214
+ var ALL_PUNCTUATION_RE = new RegExp(`(?![#*]${COMBINING_KEYCAP})\\p{P}`, "gu");
215
+ var DIGIT_SEPARATOR_RE = new RegExp(`(?<=\\p{N})[.,'${cp(1643, 1644)}](?=\\p{N})`, "gu");
216
+ var UNSPACED_LETTERS_RE = new RegExp(`(?![\\p{M}\\p{Nd}])[${UNSPACED_CLASS}]`, "gu");
217
+ var SPACE_BESIDE_UNSPACED_RE = new RegExp(
218
+ `(?<=${UNSPACED_CHARACTER}) (?=[\\p{L}\\p{M}\\p{N}])|(?<=[\\p{L}\\p{M}\\p{N}]) (?=${UNSPACED_CHARACTER})`,
219
+ "gu"
220
+ );
221
+ var WORD_RE = new RegExp(`^[\\p{L}\\p{M}\\p{N}${JOINERS}]$`, "u");
222
+ var UNSPACED_CHARACTER_RE = new RegExp(`^${UNSPACED_CHARACTER}$`, "u");
223
+ var EXTENDING_RE = new RegExp(`^[\\p{M}${JOINERS}${TAGS}]$`, "u");
224
+ var WORD = 1;
225
+ var UNSPACED = 2;
226
+ var EXTENDING = 4;
227
+ var KNOWN = 8;
228
+ var FLAGS = [];
229
+ function flagsOfCharacter(character) {
230
+ return KNOWN | (WORD_RE.test(character) ? WORD : 0) | (UNSPACED_CHARACTER_RE.test(character) ? UNSPACED : 0) | (EXTENDING_RE.test(character) ? EXTENDING : 0);
231
+ }
232
+ function flagsAt(text, index) {
233
+ const point = text.codePointAt(index);
234
+ let plane = FLAGS[point >> 16];
235
+ if (plane === void 0) {
236
+ plane = new Uint8Array(65536);
237
+ FLAGS[point >> 16] = plane;
238
+ }
239
+ const offset = point & 65535;
240
+ let flags = plane[offset];
241
+ if (flags === 0) {
242
+ flags = flagsOfCharacter(String.fromCodePoint(point));
243
+ plane[offset] = flags;
244
+ }
245
+ return flags;
246
+ }
247
+ var VIRAMAS = /* @__PURE__ */ new Set([
248
+ 2381,
249
+ 2509,
250
+ 2637,
251
+ 2765,
252
+ 2893,
253
+ 3021,
254
+ 3149,
255
+ 3277,
256
+ 3387,
257
+ 3388,
258
+ 3405,
259
+ 3530,
260
+ 3642,
261
+ 3770,
262
+ 3972,
263
+ 4153,
264
+ 4154,
265
+ 5908,
266
+ 5909,
267
+ 5940,
268
+ 6098,
269
+ 6752,
270
+ 6980,
271
+ 7082,
272
+ 7083,
273
+ 7154,
274
+ 7155,
275
+ 11647,
276
+ 43014,
277
+ 43052,
278
+ 43204,
279
+ 43347,
280
+ 43456,
281
+ 43766,
282
+ 44013,
283
+ 68159,
284
+ 69702,
285
+ 69744,
286
+ 69759,
287
+ 69817,
288
+ 69939,
289
+ 69940,
290
+ 70080,
291
+ 70197,
292
+ 70378,
293
+ 70477,
294
+ 70606,
295
+ 70607,
296
+ 70608,
297
+ 70722,
298
+ 70850,
299
+ 71103,
300
+ 71231,
301
+ 71350,
302
+ 71467,
303
+ 71737,
304
+ 71997,
305
+ 71998,
306
+ 72160,
307
+ 72244,
308
+ 72263,
309
+ 72345,
310
+ 72767,
311
+ 73028,
312
+ 73029,
313
+ 73111,
314
+ 73537,
315
+ 73538,
316
+ 90415
317
+ ]);
318
+ var JOINS_FORWARD = [
319
+ [1568, 1568],
320
+ [1574, 1574],
321
+ [1576, 1576],
322
+ [1578, 1582],
323
+ [1587, 1599],
324
+ [1601, 1607],
325
+ [1609, 1610],
326
+ [1646, 1647],
327
+ [1656, 1671],
328
+ [1690, 1727],
329
+ [1729, 1730],
330
+ [1740, 1740],
331
+ [1742, 1742],
332
+ [1744, 1745],
333
+ [1786, 1788],
334
+ [1791, 1791],
335
+ [1810, 1812],
336
+ [1818, 1821],
337
+ [1823, 1831],
338
+ [1833, 1833],
339
+ [1835, 1835],
340
+ [1837, 1838],
341
+ [1870, 1880],
342
+ [1884, 1898],
343
+ [1901, 1904],
344
+ [1906, 1906],
345
+ [1909, 1911],
346
+ [1914, 1919],
347
+ [1994, 2026],
348
+ [2113, 2117],
349
+ [2120, 2120],
350
+ [2122, 2131],
351
+ [2133, 2133],
352
+ [2144, 2144],
353
+ [2146, 2149],
354
+ [2152, 2152],
355
+ [2182, 2182],
356
+ [2185, 2189],
357
+ [2208, 2217],
358
+ [2223, 2224],
359
+ [2227, 2232],
360
+ [2234, 2248],
361
+ [6151, 6151],
362
+ [6176, 6264],
363
+ [6279, 6312],
364
+ [6314, 6314],
365
+ [43072, 43122],
366
+ [68288, 68292],
367
+ [68301, 68301],
368
+ [68307, 68316],
369
+ [68318, 68320],
370
+ [68331, 68334],
371
+ [68480, 68480],
372
+ [68482, 68482],
373
+ [68486, 68488],
374
+ [68490, 68491],
375
+ [68493, 68493],
376
+ [68496, 68496],
377
+ [68525, 68526],
378
+ [68864, 68897],
379
+ [68899, 68899],
380
+ [69315, 69316],
381
+ [69424, 69426],
382
+ [69428, 69444],
383
+ [69457, 69459],
384
+ [69488, 69491],
385
+ [69494, 69505],
386
+ [69552, 69552],
387
+ [69554, 69555],
388
+ [69560, 69560],
389
+ [69563, 69564],
390
+ [69566, 69567],
391
+ [69569, 69569],
392
+ [69572, 69572],
393
+ [69578, 69579],
394
+ [125184, 125251]
395
+ ];
396
+ var JOINS_BACKWARD = [
397
+ [1568, 1568],
398
+ [1570, 1599],
399
+ [1601, 1610],
400
+ [1646, 1647],
401
+ [1649, 1651],
402
+ [1653, 1747],
403
+ [1749, 1749],
404
+ [1774, 1775],
405
+ [1786, 1788],
406
+ [1791, 1791],
407
+ [1808, 1808],
408
+ [1810, 1839],
409
+ [1869, 1919],
410
+ [1994, 2026],
411
+ [2112, 2136],
412
+ [2144, 2144],
413
+ [2146, 2149],
414
+ [2151, 2154],
415
+ [2160, 2178],
416
+ [2182, 2182],
417
+ [2185, 2190],
418
+ [2208, 2220],
419
+ [2222, 2248],
420
+ [6151, 6151],
421
+ [6176, 6264],
422
+ [6279, 6312],
423
+ [6314, 6314],
424
+ [43072, 43121],
425
+ [68288, 68293],
426
+ [68295, 68295],
427
+ [68297, 68298],
428
+ [68302, 68310],
429
+ [68312, 68321],
430
+ [68324, 68324],
431
+ [68331, 68335],
432
+ [68480, 68497],
433
+ [68521, 68526],
434
+ [68865, 68899],
435
+ [69314, 69316],
436
+ [69424, 69444],
437
+ [69457, 69460],
438
+ [69488, 69505],
439
+ [69552, 69552],
440
+ [69554, 69558],
441
+ [69560, 69567],
442
+ [69569, 69572],
443
+ [69577, 69578],
444
+ [125184, 125251]
445
+ ];
446
+ var TRANSPARENT_RE = new RegExp(`^[\\p{Mn}\\p{Me}${cp(125259)}${TAGS}]$`, "u");
447
+ function collapseWhitespace(text) {
448
+ return text.replace(WHITESPACE_RE, " ").trim();
449
+ }
450
+ function codePointLength(text) {
451
+ let length = 0;
452
+ for (const _ of text) {
453
+ length += 1;
454
+ }
455
+ return length;
456
+ }
457
+ function truncateCodePoints(text, max) {
458
+ if (text.length <= max) {
459
+ return text;
460
+ }
461
+ let points = 0;
462
+ let units = 0;
463
+ for (const character of text) {
464
+ if (points === max) {
465
+ return text.slice(0, units);
466
+ }
467
+ points += 1;
468
+ units += character.length;
469
+ }
470
+ return text;
471
+ }
472
+ function isLongerThan(text, max) {
473
+ if (text.length <= max) {
474
+ return false;
475
+ }
476
+ return text.length > 2 * max || truncateCodePoints(text, max).length < text.length;
477
+ }
478
+ function inRanges(point, ranges) {
479
+ return ranges.some(([first, last]) => point >= first && point <= last);
480
+ }
481
+ function isMongolianLetterOrMark(point) {
482
+ return point !== void 0 && point >= 6176 && point <= 6314;
483
+ }
484
+ function isMongolianSelector(point) {
485
+ return point !== void 0 && (point >= 6155 && point <= 6157 || point === 6159);
486
+ }
487
+ function letterBefore(points, index) {
488
+ for (let at = index - 1; at >= 0; at -= 1) {
489
+ const point = points[at];
490
+ if (!TRANSPARENT_RE.test(point)) {
491
+ return point.codePointAt(0);
492
+ }
493
+ }
494
+ return void 0;
495
+ }
496
+ function letterAfter(points, index) {
497
+ for (let at = index + 1; at < points.length; at += 1) {
498
+ const point = points[at];
499
+ if (!TRANSPARENT_RE.test(point)) {
500
+ return point.codePointAt(0);
501
+ }
502
+ }
503
+ return void 0;
504
+ }
505
+ var LETTER_OR_MARK_RE = /^[\p{L}\p{M}]$/u;
506
+ function keepsInContext(points, index) {
507
+ const point = points[index].codePointAt(0);
508
+ const previous = points[index - 1];
509
+ const afterVirama = previous !== void 0 && VIRAMAS.has(previous.codePointAt(0));
510
+ if (point === ZWJ) {
511
+ const next = points[index + 1];
512
+ return afterVirama || previous !== void 0 && LETTER_OR_MARK_RE.test(previous) && next !== void 0 && VIRAMAS.has(next.codePointAt(0));
513
+ }
514
+ if (point === ZWNJ) {
515
+ if (afterVirama) {
516
+ return true;
517
+ }
518
+ const before = letterBefore(points, index);
519
+ const after = letterAfter(points, index);
520
+ return before !== void 0 && after !== void 0 && inRanges(before, JOINS_FORWARD) && inRanges(after, JOINS_BACKWARD);
521
+ }
522
+ const previousPoint = previous?.codePointAt(0);
523
+ if (point === MONGOLIAN_VOWEL_SEPARATOR) {
524
+ return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(points[index + 1]?.codePointAt(0));
525
+ }
526
+ return isMongolianLetterOrMark(previousPoint);
527
+ }
528
+ function settle(text) {
529
+ let current = text;
530
+ for (; ; ) {
531
+ let next = foldSequences(current);
532
+ if (CONTEXTUAL_RE.test(next)) {
533
+ const points = Array.from(next.replace(REPEATED_JOINER_RE, "$1"));
534
+ let inFlag = false;
535
+ next = points.filter((character, index) => {
536
+ const point = character.codePointAt(0);
537
+ if (point >= 917536 && point <= 917631) {
538
+ return inFlag;
539
+ }
540
+ inFlag = point === WAVING_BLACK_FLAG;
541
+ return !CONTEXTUAL_RE.test(character) || keepsInContext(points, index);
542
+ }).join("");
543
+ }
544
+ next = collapseWhitespace(next).normalize("NFC");
545
+ if (next === current) {
546
+ return current;
547
+ }
548
+ current = next;
549
+ }
550
+ }
551
+ var CARRIED_MARKS_RE = new RegExp(`^[ ${cp(1600)}]?(\\p{M}+)$`, "u");
552
+ var SPACING_CHARACTER_RE = new RegExp(`[\\s${NEXT_LINE}]`, "u");
553
+ var PUNCTUATION_CHARACTER_RE = /^\p{P}$/u;
554
+ function foldCompatibility(character, offset, text) {
555
+ const folded = character.normalize("NFKC");
556
+ const carried = CARRIED_MARKS_RE.exec(folded);
557
+ if (carried === null) {
558
+ return COMPATIBILITY_JAMO.get(folded) ?? folded;
559
+ }
560
+ return PUNCTUATION_CHARACTER_RE.test(character) || offset === 0 || SPACING_CHARACTER_RE.test(text.charAt(offset - 1)) ? character : carried[1];
561
+ }
562
+ function preStripNormalize(text) {
563
+ return settle(
564
+ collapseWhitespace(
565
+ text.replace(LONE_SURROGATE_RE, REPLACEMENT_CHARACTER).normalize("NFC").replace(COMPATIBILITY_RE, foldCompatibility).replace(INVISIBLE_RE, "").replace(FOLDED_RE, (character) => FOLDED.get(character)).toLowerCase()
566
+ ).normalize("NFC")
567
+ );
568
+ }
569
+ function settleWithout(text, pattern, replacement) {
570
+ let current = text;
571
+ for (; ; ) {
572
+ const next = settle(current.replace(pattern, replacement));
573
+ if (next === current) {
574
+ return current;
575
+ }
576
+ current = next;
577
+ }
578
+ }
579
+ function stripPunctuation(text) {
580
+ return settleWithout(text, PUNCTUATION_RE, "");
581
+ }
582
+ function spacePunctuation(text) {
583
+ return settleWithout(text.replace(DIGIT_SEPARATOR_RE, ""), ALL_PUNCTUATION_RE, " ");
584
+ }
585
+ function isHighSurrogate(unit) {
586
+ return unit >= 55296 && unit <= 56319;
587
+ }
588
+ function isLowSurrogate(unit) {
589
+ return unit >= 56320 && unit <= 57343;
590
+ }
591
+ function startOfCodePointBefore(text, index) {
592
+ return index >= 2 && isLowSurrogate(text.charCodeAt(index - 1)) && isHighSurrogate(text.charCodeAt(index - 2)) ? index - 2 : index - 1;
593
+ }
594
+ function lastCodePointAfter(previous, appended) {
595
+ return appended === "" ? previous : appended.slice(startOfCodePointBefore(appended, appended.length));
596
+ }
597
+ function isAsciiWordUnit(unit) {
598
+ return unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122;
599
+ }
600
+ function isWordAt(text, index) {
601
+ const unit = text.charCodeAt(index);
602
+ return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & WORD) !== 0;
603
+ }
604
+ function isSpacedWordAt(text, index) {
605
+ const unit = text.charCodeAt(index);
606
+ return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & (WORD | UNSPACED)) === WORD;
607
+ }
608
+ function isExtendingAt(text, index) {
609
+ return text.charCodeAt(index) >= 128 && (flagsAt(text, index) & EXTENDING) !== 0;
610
+ }
611
+ function edgeOf(text, index, unspaced) {
612
+ if (!isWordAt(text, index)) {
613
+ return "none";
614
+ }
615
+ return isSpacedWordAt(text, index) ? "spaced" : unspaced;
616
+ }
617
+ function phraseOf(text, unspaced) {
618
+ return {
619
+ text,
620
+ before: edgeOf(text, 0, unspaced),
621
+ after: edgeOf(text, startOfCodePointBefore(text, text.length), unspaced)
622
+ };
623
+ }
624
+ function blocks(edge, text, neighbour) {
625
+ if (edge === "none") {
626
+ return false;
627
+ }
628
+ return edge === "spaced" ? isSpacedWordAt(text, neighbour) : isWordAt(text, neighbour);
629
+ }
630
+ function isMatchAt(text, phrase, at) {
631
+ const end = at + phrase.text.length;
632
+ if (end < text.length && (isExtendingAt(text, end) || blocks(phrase.after, text, end))) {
633
+ return false;
634
+ }
635
+ return !(at > 0 && blocks(phrase.before, text, startOfCodePointBefore(text, at)));
636
+ }
637
+ function indexOfPhrase(text, phrase, start) {
638
+ const needle = phrase.text;
639
+ let at = text.indexOf(needle, start);
640
+ while (at !== -1) {
641
+ if (phrase.before === "spaced" && at > 0 && isSpacedWordAt(text, startOfCodePointBefore(text, at))) {
642
+ let next = at;
643
+ while (next < text.length && isSpacedWordAt(text, next)) {
644
+ next += isHighSurrogate(text.charCodeAt(next)) && isLowSurrogate(text.charCodeAt(next + 1)) ? 2 : 1;
645
+ }
646
+ at = text.indexOf(needle, next);
647
+ continue;
648
+ }
649
+ if (isMatchAt(text, phrase, at)) {
650
+ return at;
651
+ }
652
+ at = text.indexOf(needle, at + 1);
653
+ }
654
+ return -1;
655
+ }
656
+ function containsPhrase(text, phrase) {
657
+ const needle = phrase.text;
658
+ const length = needle.length;
659
+ if (length === 0 || length > text.length) {
660
+ return false;
661
+ }
662
+ const fallback = new Int32Array(length);
663
+ for (let index = 1, matched = 0; index < length; index += 1) {
664
+ while (matched > 0 && needle.charCodeAt(index) !== needle.charCodeAt(matched)) {
665
+ matched = fallback[matched - 1];
666
+ }
667
+ if (needle.charCodeAt(index) === needle.charCodeAt(matched)) {
668
+ matched += 1;
669
+ }
670
+ fallback[index] = matched;
671
+ }
672
+ for (let index = 0, matched = 0; index < text.length; index += 1) {
673
+ while (matched > 0 && text.charCodeAt(index) !== needle.charCodeAt(matched)) {
674
+ matched = fallback[matched - 1];
675
+ }
676
+ if (text.charCodeAt(index) === needle.charCodeAt(matched)) {
677
+ matched += 1;
678
+ }
679
+ if (matched === length) {
680
+ if (isMatchAt(text, phrase, index - length + 1)) {
681
+ return true;
682
+ }
683
+ matched = fallback[matched - 1];
684
+ }
685
+ }
686
+ return false;
687
+ }
688
+ function compileRules(tolerance) {
689
+ const equivalences = tolerance?.equivalences;
690
+ if (!Array.isArray(equivalences)) {
691
+ return { rules: [], signature: "[]" };
692
+ }
693
+ const rules = [];
694
+ for (const rule of equivalences.slice(0, DICTATION_MAX_EQUIVALENCES)) {
695
+ const rawFrom = rule?.from;
696
+ const rawTo = rule?.to;
697
+ if (typeof rawFrom !== "string" || typeof rawTo !== "string" || isLongerThan(rawFrom, DICTATION_MAX_EQUIVALENCE_LENGTH) || isLongerThan(rawTo, DICTATION_MAX_EQUIVALENCE_LENGTH)) {
698
+ continue;
699
+ }
700
+ const from = preStripNormalize(rawFrom);
701
+ const to = stripPunctuation(preStripNormalize(rawTo));
702
+ if (from === "" || to === "") {
703
+ continue;
704
+ }
705
+ rules.push({
706
+ from: phraseOf(from, "none"),
707
+ to,
708
+ spaceBefore: isSpacedWordAt(to, 0),
709
+ spaceAfter: isSpacedWordAt(to, startOfCodePointBefore(to, to.length))
710
+ });
711
+ }
712
+ return {
713
+ rules,
714
+ signature: JSON.stringify(rules.map((rule) => [rule.from.text, rule.to]))
715
+ };
716
+ }
717
+ function bounded(text) {
718
+ return isLongerThan(text, WORKING_LENGTH) ? { text: truncateCodePoints(text, WORKING_LENGTH), cut: true } : { text, cut: false };
719
+ }
720
+ function rewrite(text, rule) {
721
+ let output = "";
722
+ let consumed = 0;
723
+ let last = "";
724
+ for (let at = indexOfPhrase(text, rule.from, 0); at !== -1; at = indexOfPhrase(text, rule.from, consumed)) {
725
+ const between = text.slice(consumed, at);
726
+ if (rule.spaceAfter && consumed > 0 && between !== "" && isSpacedWordAt(between, 0)) {
727
+ output += " ";
728
+ }
729
+ output += between;
730
+ last = lastCodePointAfter(last, between);
731
+ if (rule.spaceBefore && last !== "" && isSpacedWordAt(last, 0)) {
732
+ output += " ";
733
+ last = " ";
734
+ }
735
+ output += rule.to;
736
+ last = lastCodePointAfter(last, rule.to);
737
+ consumed = at + rule.from.text.length;
738
+ if (output.length > 2 * WORKING_LENGTH) {
739
+ return { text: truncateCodePoints(output, WORKING_LENGTH), cut: true };
740
+ }
741
+ }
742
+ const rest = text.slice(consumed);
743
+ const apart = rule.spaceAfter && consumed > 0 && rest !== "" && isSpacedWordAt(rest, 0);
744
+ return bounded(apart ? `${output} ${rest}` : output + rest);
745
+ }
746
+ var WORKING_TEXTS = /* @__PURE__ */ Symbol("dictation working texts");
747
+ var MEASURED_TRANSCRIPTS = /* @__PURE__ */ Symbol("dictation measured transcripts");
748
+ var SHOWN_TEXTS = /* @__PURE__ */ Symbol("dictation shown texts");
749
+ var REVEALS = /* @__PURE__ */ Symbol("dictation reveal answers");
750
+ function scopedMap(key, signature) {
751
+ const bySignature = scopeCache(key, () => /* @__PURE__ */ new Map());
752
+ if (bySignature === void 0) {
753
+ return /* @__PURE__ */ new Map();
754
+ }
755
+ let map = bySignature.get(signature);
756
+ if (map === void 0) {
757
+ map = /* @__PURE__ */ new Map();
758
+ bySignature.set(signature, map);
759
+ }
760
+ return map;
761
+ }
762
+ var PRE_STRIP_TEXTS = /* @__PURE__ */ Symbol("dictation pre-strip texts");
763
+ function boundedPreStrip(text) {
764
+ const memo = scopeCache(PRE_STRIP_TEXTS, () => /* @__PURE__ */ new Map());
765
+ let result = memo?.get(text);
766
+ if (result === void 0) {
767
+ result = bounded(preStripNormalize(text));
768
+ memo?.set(text, result);
769
+ }
770
+ return result;
771
+ }
772
+ function isBlankBeforeRules(text) {
773
+ return boundedPreStrip(text).text === "";
774
+ }
775
+ function workingText(text, compiled, preStripped = boundedPreStrip(text)) {
776
+ if (compiled.rules.length === 0) {
777
+ return preStripped;
778
+ }
779
+ const memo = scopeCache(WORKING_TEXTS, () => /* @__PURE__ */ new Map());
780
+ let known = memo?.get(compiled.signature);
781
+ const remembered = known?.get(text);
782
+ if (remembered !== void 0) {
783
+ return remembered;
784
+ }
785
+ let working = preStripped;
786
+ let cut = working.cut;
787
+ for (const rule of compiled.rules) {
788
+ working = rewrite(working.text, rule);
789
+ cut ||= working.cut;
790
+ }
791
+ const result = { text: working.text, cut };
792
+ if (memo !== void 0) {
793
+ if (known === void 0) {
794
+ known = /* @__PURE__ */ new Map();
795
+ memo.set(compiled.signature, known);
796
+ }
797
+ known.set(text, result);
798
+ }
799
+ return result;
800
+ }
801
+ function normalizeDictationTextBounded(text, tolerance) {
802
+ const working = workingText(text, compileRules(tolerance));
803
+ return { text: stripPunctuation(working.text), cut: working.cut };
804
+ }
805
+ function normalizeDictationText(text, tolerance) {
806
+ return normalizeDictationTextBounded(text, tolerance).text;
807
+ }
808
+ var NOTHING_TO_REVEAL = { forms: [], long: false, before: "none", after: "none" };
809
+ function stripForReveal(text) {
810
+ return stripPunctuation(text.replace(DIGIT_SEPARATOR_RE, ""));
811
+ }
812
+ function readingOf(written, working) {
813
+ const writtenStripped = stripForReveal(written);
814
+ const workingStripped = stripForReveal(working);
815
+ const source = writtenStripped === "" ? workingStripped : writtenStripped;
816
+ if (source === "") {
817
+ return NOTHING_TO_REVEAL;
818
+ }
819
+ const long = (source.match(UNSPACED_LETTERS_RE)?.length ?? 0) >= UNSPACED_REVEAL_MIN_CHARACTERS;
820
+ const { before, after } = phraseOf(source, long ? "none" : "visible");
821
+ const forms = [
822
+ writtenStripped,
823
+ spacePunctuation(written),
824
+ workingStripped,
825
+ spacePunctuation(working)
826
+ ].map((form) => long ? form.replace(SPACE_BESIDE_UNSPACED_RE, "") : form);
827
+ return { forms: [...new Set(forms)].filter((form) => form !== ""), long, before, after };
828
+ }
829
+ function dictationNormalizer(tolerance) {
830
+ const compiled = compileRules(tolerance);
831
+ const measured = scopedMap(MEASURED_TRANSCRIPTS, compiled.signature);
832
+ const shownTexts = scopedMap(SHOWN_TEXTS, compiled.signature);
833
+ return {
834
+ measure(text) {
835
+ let result = measured.get(text);
836
+ if (result === void 0) {
837
+ if (isLongerThan(text, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
838
+ result = { tooLong: true, normalized: null, reveal: NOTHING_TO_REVEAL };
839
+ } else {
840
+ const written = boundedPreStrip(text);
841
+ const working = workingText(text, compiled, written);
842
+ const normalized = stripPunctuation(working.text);
843
+ result = {
844
+ tooLong: working.cut || isLongerThan(normalized, DICTATION_MAX_TRANSCRIPT_LENGTH),
845
+ normalized,
846
+ reveal: readingOf(written.text, working.text)
847
+ };
848
+ }
849
+ measured.set(text, result);
850
+ }
851
+ return result;
852
+ },
853
+ shown(text) {
854
+ let result = shownTexts.get(text);
855
+ if (result === void 0) {
856
+ const plainText = isLongerThan(text, WORKING_LENGTH) ? null : boundedPreStrip(text);
857
+ const working = plainText === null ? null : workingText(text, compiled, plainText);
858
+ if (plainText === null || working === null || working.cut) {
859
+ result = { tooLong: true, forms: [] };
860
+ } else {
861
+ const plain = plainText.text;
862
+ result = {
863
+ tooLong: false,
864
+ forms: [
865
+ .../* @__PURE__ */ new Set([
866
+ stripForReveal(working.text),
867
+ spacePunctuation(working.text),
868
+ stripForReveal(plain),
869
+ spacePunctuation(plain)
870
+ ])
871
+ ]
872
+ };
873
+ }
874
+ shownTexts.set(text, result);
875
+ }
876
+ return result;
877
+ }
878
+ };
879
+ }
880
+ function revealsCandidate(forms, candidate) {
881
+ const memo = scopeCache(REVEALS, () => /* @__PURE__ */ new WeakMap());
882
+ const known = memo?.get(forms)?.get(candidate);
883
+ if (known !== void 0) {
884
+ return known;
885
+ }
886
+ const answer = searchReveal(forms, candidate);
887
+ if (memo !== void 0) {
888
+ const byCandidate = memo.get(forms) ?? /* @__PURE__ */ new WeakMap();
889
+ byCandidate.set(candidate, answer);
890
+ memo.set(forms, byCandidate);
891
+ }
892
+ return answer;
893
+ }
894
+ var COMPACTED_FORMS = /* @__PURE__ */ new WeakMap();
895
+ function searchReveal(forms, candidate) {
896
+ const { reveal } = candidate;
897
+ if (candidate.tooLong) {
898
+ return false;
899
+ }
900
+ let fields = forms;
901
+ if (reveal.long) {
902
+ fields = COMPACTED_FORMS.get(forms) ?? forms.map((form) => form.replace(SPACE_BESIDE_UNSPACED_RE, ""));
903
+ COMPACTED_FORMS.set(forms, fields);
904
+ }
905
+ return reveal.forms.some((text) => {
906
+ const phrase = { text, before: reveal.before, after: reveal.after };
907
+ return fields.some((field) => containsPhrase(field, phrase));
908
+ });
909
+ }
910
+
41
911
  // src/schemas/feedback.ts
42
912
  import { z } from "zod/v4";
43
913
  var FeedbackSchema = z.looseObject({
@@ -136,47 +1006,264 @@ var RedactedMediaSchema = z2.strictObject({
136
1006
  path: ["url"]
137
1007
  });
138
1008
 
139
- // src/schemas/fill-in-the-blanks.ts
1009
+ // src/schemas/dictation.ts
140
1010
  import { z as z3 } from "zod/v4";
141
- var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
142
- var TextMatchPolicySchema = z3.looseObject({
143
- caseSensitive: z3.boolean().optional(),
144
- trim: z3.boolean().optional(),
145
- normalize: z3.enum(["none", "NFC", "NFKC"]).optional(),
146
- foldDiacritics: z3.boolean().optional(),
147
- collapseInnerWhitespace: z3.boolean().optional(),
148
- ignorePunctuation: z3.boolean().optional(),
149
- levenshtein: z3.number().int().min(0).optional(),
150
- locale: z3.string().optional()
1011
+ var DictationSlowMediaSchema = z3.looseObject({
1012
+ type: z3.literal("audio"),
1013
+ url: MediaUrlSchema,
1014
+ alt: z3.string().min(1).optional()
151
1015
  });
152
- var BlankConfigSchema = z3.looseObject({
153
- id: z3.string().min(1),
154
- acceptedAnswers: z3.array(
155
- z3.string().min(1).refine((answer) => answer.trim().length > 0, {
156
- error: "Accepted answers must contain non-whitespace characters."
157
- })
158
- ).min(1),
159
- caseSensitive: z3.boolean().optional(),
160
- trimWhitespace: z3.boolean().optional(),
161
- match: TextMatchPolicySchema.optional(),
162
- hint: z3.string().optional(),
163
- feedback: z3.string().optional()
1016
+ var RuleTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1017
+ var TranscriptTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1018
+ var DictationEquivalenceSchema = z3.looseObject({
1019
+ from: RuleTextSchema,
1020
+ to: RuleTextSchema
1021
+ }).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1022
+ error: `A rule rewrites at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1023
+ path: ["from"]
1024
+ }).refine((rule) => codePointLength(rule.to) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1025
+ error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1026
+ path: ["to"]
1027
+ });
1028
+ var DictationToleranceSchema = z3.looseObject({
1029
+ equivalences: z3.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
1030
+ error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
1031
+ }).optional()
164
1032
  });
165
- var FillInTheBlanksDataSchema = z3.looseObject({
1033
+ var normalizers = /* @__PURE__ */ new WeakMap();
1034
+ function normalizerFor(data) {
1035
+ let normalizer = normalizers.get(data);
1036
+ if (normalizer === void 0) {
1037
+ normalizer = dictationNormalizer(data.tolerance);
1038
+ normalizers.set(data, normalizer);
1039
+ }
1040
+ return normalizer;
1041
+ }
1042
+ function measure(data, text) {
1043
+ return normalizerFor(data).measure(text);
1044
+ }
1045
+ function candidatesOf(data) {
1046
+ return [data.transcript, ...data.acceptedTranscripts ?? []];
1047
+ }
1048
+ function isEmpty(measured) {
1049
+ return measured.normalized === "";
1050
+ }
1051
+ function checkEvenAfterIssues(check) {
1052
+ const guard = new z3.core.$ZodCheck({ check: "custom", when: () => true });
1053
+ guard._zod.check = check;
1054
+ return guard;
1055
+ }
1056
+ var DictationDataShape = z3.looseObject({
166
1057
  schemaVersion: z3.literal("1.0"),
167
- type: z3.literal("fill-in-the-blanks"),
1058
+ type: z3.literal("dictation"),
168
1059
  id: z3.string().min(1),
169
1060
  title: z3.string().min(1),
170
- passage: z3.string().min(1),
171
- passageHtml: z3.string().optional(),
172
- blanks: z3.array(BlankConfigSchema).min(1),
173
- scoringStrategy: z3.enum(["all-or-nothing", "partial"]),
1061
+ transcript: TranscriptTextSchema,
1062
+ acceptedTranscripts: z3.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
174
1063
  media: MediaSchema.optional(),
1064
+ slowMedia: DictationSlowMediaSchema.optional(),
1065
+ hints: z3.looseObject({ mode: z3.literal("progressive-words") }).optional(),
1066
+ tolerance: DictationToleranceSchema.optional(),
175
1067
  feedback: FeedbackSchema.optional(),
176
1068
  passThreshold: z3.number().min(0).max(1).optional(),
177
1069
  locale: z3.string().optional(),
178
1070
  learningObjectives: z3.array(z3.string()).optional(),
179
1071
  difficultyLevel: z3.literal([1, 2, 3, 4, 5]).optional()
1072
+ });
1073
+ var DictationDataSchema = DictationDataShape.check(
1074
+ checkEvenAfterIssues((ctx) => {
1075
+ if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
1076
+ return;
1077
+ }
1078
+ const data = ctx.value;
1079
+ const refused = new Set(ctx.issues.map((issue2) => String(issue2.path?.[0])));
1080
+ const report = (path, input, message) => {
1081
+ ctx.issues.push({ code: "custom", input, message, path: [...path] });
1082
+ };
1083
+ const rulesParse = !refused.has("tolerance");
1084
+ const transcriptParses = !refused.has("transcript");
1085
+ if (rulesParse && transcriptParses) {
1086
+ const transcript = measure(data, data.transcript);
1087
+ if (isEmpty(transcript)) {
1088
+ report(
1089
+ ["transcript"],
1090
+ data.transcript,
1091
+ "The transcript must contain something to type: after ignoring case, punctuation and spacing nothing is left."
1092
+ );
1093
+ }
1094
+ if (transcript.tooLong) {
1095
+ report(
1096
+ ["transcript"],
1097
+ data.transcript,
1098
+ `A transcript is at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
1099
+ );
1100
+ }
1101
+ if (!refused.has("acceptedTranscripts")) {
1102
+ const accepted = data.acceptedTranscripts ?? [];
1103
+ if (accepted.some((entry) => {
1104
+ const measured = measure(data, entry);
1105
+ return measured.tooLong || isEmpty(measured);
1106
+ })) {
1107
+ report(
1108
+ ["acceptedTranscripts"],
1109
+ data.acceptedTranscripts,
1110
+ `An accepted transcript must contain something to type and be at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
1111
+ );
1112
+ }
1113
+ const normalized = candidatesOf(data).map((entry) => measure(data, entry).normalized).filter((entry) => entry !== null);
1114
+ if (new Set(normalized).size !== normalized.length) {
1115
+ report(
1116
+ ["acceptedTranscripts"],
1117
+ data.acceptedTranscripts,
1118
+ "An accepted transcript must differ from the transcript and from the other accepted transcripts once case, punctuation and spacing are ignored."
1119
+ );
1120
+ }
1121
+ }
1122
+ }
1123
+ const mediaParses = !refused.has("media");
1124
+ const slowParses = !refused.has("slowMedia");
1125
+ if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
1126
+ report(
1127
+ ["media", "type"],
1128
+ data.media.type,
1129
+ "A dictation is dictated: its recording must be audio."
1130
+ );
1131
+ }
1132
+ if (slowParses && data.slowMedia?.playback !== void 0) {
1133
+ report(
1134
+ ["slowMedia", "playback"],
1135
+ data.slowMedia.playback,
1136
+ "The slow recording carries no playback policy of its own; put the policy on `media` \u2014 the slow recording follows it (except the play budget)."
1137
+ );
1138
+ }
1139
+ if (mediaParses && slowParses && data.slowMedia !== void 0) {
1140
+ if (data.media?.playback?.maxPlays !== void 0) {
1141
+ report(
1142
+ ["slowMedia"],
1143
+ data.slowMedia,
1144
+ "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays` (a budgeted recording can still offer the transport's 0.75\xD7 speed when `rate` is `allow`)."
1145
+ );
1146
+ }
1147
+ if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
1148
+ report(
1149
+ ["slowMedia", "url"],
1150
+ data.slowMedia.url,
1151
+ "The slow recording must be a different file from the recording."
1152
+ );
1153
+ }
1154
+ if (data.media === void 0) {
1155
+ report(
1156
+ ["media"],
1157
+ data.media,
1158
+ "A slow recording accompanies a recording: add `media` first."
1159
+ );
1160
+ }
1161
+ }
1162
+ if (rulesParse && !(data.tolerance?.equivalences ?? []).every(
1163
+ (rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
1164
+ )) {
1165
+ report(
1166
+ ["tolerance", "equivalences"],
1167
+ data.tolerance?.equivalences,
1168
+ "An equivalence must name a word, phrase or symbol to rewrite, and rewrite it to something that survives normalisation (not only punctuation or spaces)."
1169
+ );
1170
+ }
1171
+ for (const field of ["media", "slowMedia"]) {
1172
+ const recording = data[field];
1173
+ if (recording?.captionsUrl !== void 0) {
1174
+ report(
1175
+ [field, "captionsUrl"],
1176
+ recording.captionsUrl,
1177
+ "A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
1178
+ );
1179
+ }
1180
+ }
1181
+ if (!rulesParse || !transcriptParses) {
1182
+ return;
1183
+ }
1184
+ const normalizer = normalizerFor(data);
1185
+ const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
1186
+ const shown = [
1187
+ [["title"], refused.has("title") ? void 0 : data.title],
1188
+ [["media", "alt"], data.media?.alt],
1189
+ [["slowMedia", "alt"], data.slowMedia?.alt]
1190
+ ];
1191
+ for (const [path, value] of shown) {
1192
+ if (value === void 0) {
1193
+ continue;
1194
+ }
1195
+ const { tooLong, forms } = normalizer.shown(value);
1196
+ if (tooLong) {
1197
+ report(
1198
+ path,
1199
+ value,
1200
+ `The title and the recording descriptions are searched for the transcript before they are shown: each is at most ${WORKING_LENGTH} characters, before and after the equivalences are applied, and the equivalences may not grow it past that on the way.`
1201
+ );
1202
+ } else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
1203
+ report(
1204
+ path,
1205
+ value,
1206
+ 'The title and the recording descriptions are shown to the learner before they type: they must not contain the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
1207
+ );
1208
+ }
1209
+ }
1210
+ })
1211
+ );
1212
+ function groupCaptionsRevealDictation(group, hasCaptions = (captionsUrl) => captionsUrl !== void 0) {
1213
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1214
+ if (!isObject(group) || !isObject(group.stimulus) || !isObject(group.stimulus.media)) {
1215
+ return false;
1216
+ }
1217
+ if (!hasCaptions(group.stimulus.media.captionsUrl) || !Array.isArray(group.items)) {
1218
+ return false;
1219
+ }
1220
+ return group.items.some(
1221
+ (item) => isObject(item) && item.type === "dictation" && item.media === void 0
1222
+ );
1223
+ }
1224
+ var GROUP_CAPTIONS_REVEAL_DICTATION = "The stimulus recording carries captions, and a dictation in this group plays that recording: the captions are the answer. Remove the captions, or give the dictation its own recording.";
1225
+
1226
+ // src/schemas/fill-in-the-blanks.ts
1227
+ import { z as z4 } from "zod/v4";
1228
+ var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
1229
+ var TextMatchPolicySchema = z4.looseObject({
1230
+ caseSensitive: z4.boolean().optional(),
1231
+ trim: z4.boolean().optional(),
1232
+ normalize: z4.enum(["none", "NFC", "NFKC"]).optional(),
1233
+ foldDiacritics: z4.boolean().optional(),
1234
+ collapseInnerWhitespace: z4.boolean().optional(),
1235
+ ignorePunctuation: z4.boolean().optional(),
1236
+ levenshtein: z4.number().int().min(0).optional(),
1237
+ locale: z4.string().optional()
1238
+ });
1239
+ var BlankConfigSchema = z4.looseObject({
1240
+ id: z4.string().min(1),
1241
+ acceptedAnswers: z4.array(
1242
+ z4.string().min(1).refine((answer) => answer.trim().length > 0, {
1243
+ error: "Accepted answers must contain non-whitespace characters."
1244
+ })
1245
+ ).min(1),
1246
+ caseSensitive: z4.boolean().optional(),
1247
+ trimWhitespace: z4.boolean().optional(),
1248
+ match: TextMatchPolicySchema.optional(),
1249
+ hint: z4.string().optional(),
1250
+ feedback: z4.string().optional()
1251
+ });
1252
+ var FillInTheBlanksDataSchema = z4.looseObject({
1253
+ schemaVersion: z4.literal("1.0"),
1254
+ type: z4.literal("fill-in-the-blanks"),
1255
+ id: z4.string().min(1),
1256
+ title: z4.string().min(1),
1257
+ passage: z4.string().min(1),
1258
+ passageHtml: z4.string().optional(),
1259
+ blanks: z4.array(BlankConfigSchema).min(1),
1260
+ scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
1261
+ media: MediaSchema.optional(),
1262
+ feedback: FeedbackSchema.optional(),
1263
+ passThreshold: z4.number().min(0).max(1).optional(),
1264
+ locale: z4.string().optional(),
1265
+ learningObjectives: z4.array(z4.string()).optional(),
1266
+ difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
180
1267
  }).refine(
181
1268
  (data) => {
182
1269
  const placeholderCounts2 = /* @__PURE__ */ new Map();
@@ -200,14 +1287,14 @@ var FillInTheBlanksDataSchema = z3.looseObject({
200
1287
  );
201
1288
 
202
1289
  // src/schemas/multiple-choice.ts
203
- import { z as z4 } from "zod/v4";
204
- var MultipleChoiceOptionMediaSchema = z4.looseObject({
1290
+ import { z as z5 } from "zod/v4";
1291
+ var MultipleChoiceOptionMediaSchema = z5.looseObject({
205
1292
  // No `embed`: an iframe swallows the click that selects the option, so the
206
1293
  // learner could not choose it. No `video`: a native control bar inside the
207
1294
  // option's label eats the same click.
208
- type: z4.enum(["image", "audio"]),
1295
+ type: z5.enum(["image", "audio"]),
209
1296
  url: MediaUrlSchema,
210
- alt: z4.string().min(1).optional(),
1297
+ alt: z5.string().min(1).optional(),
211
1298
  captionsUrl: MediaUrlSchema.optional()
212
1299
  }).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
213
1300
  error: "An image option requires a non-empty `alt`.",
@@ -216,30 +1303,30 @@ var MultipleChoiceOptionMediaSchema = z4.looseObject({
216
1303
  error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
217
1304
  path: ["playback"]
218
1305
  });
219
- var MultipleChoiceOptionSchema = z4.looseObject({
220
- id: z4.string().min(1),
221
- text: z4.string().min(1),
222
- isCorrect: z4.boolean(),
223
- feedback: z4.string().optional(),
1306
+ var MultipleChoiceOptionSchema = z5.looseObject({
1307
+ id: z5.string().min(1),
1308
+ text: z5.string().min(1),
1309
+ isCorrect: z5.boolean(),
1310
+ feedback: z5.string().optional(),
224
1311
  media: MultipleChoiceOptionMediaSchema.optional()
225
1312
  });
226
- var MultipleChoiceDataSchema = z4.looseObject({
227
- schemaVersion: z4.literal("1.0"),
228
- type: z4.literal("multiple-choice"),
229
- id: z4.string().min(1),
230
- title: z4.string().min(1),
231
- question: z4.string().min(1),
232
- questionHtml: z4.string().optional(),
233
- mode: z4.enum(["single", "multi"]),
234
- options: z4.array(MultipleChoiceOptionSchema).min(2).max(26),
235
- scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
1313
+ var MultipleChoiceDataSchema = z5.looseObject({
1314
+ schemaVersion: z5.literal("1.0"),
1315
+ type: z5.literal("multiple-choice"),
1316
+ id: z5.string().min(1),
1317
+ title: z5.string().min(1),
1318
+ question: z5.string().min(1),
1319
+ questionHtml: z5.string().optional(),
1320
+ mode: z5.enum(["single", "multi"]),
1321
+ options: z5.array(MultipleChoiceOptionSchema).min(2).max(26),
1322
+ scoringStrategy: z5.enum(["all-or-nothing", "partial"]),
236
1323
  media: MediaSchema.optional(),
237
1324
  feedback: FeedbackSchema.optional(),
238
- passThreshold: z4.number().min(0).max(1).optional(),
239
- shuffle: z4.boolean().optional(),
240
- locale: z4.string().optional(),
241
- learningObjectives: z4.array(z4.string()).optional(),
242
- difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
1325
+ passThreshold: z5.number().min(0).max(1).optional(),
1326
+ shuffle: z5.boolean().optional(),
1327
+ locale: z5.string().optional(),
1328
+ learningObjectives: z5.array(z5.string()).optional(),
1329
+ difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional()
243
1330
  }).refine((data) => data.options.some((option) => option.isCorrect), {
244
1331
  error: "At least one option must be marked correct.",
245
1332
  path: ["options"]
@@ -267,21 +1354,21 @@ function countWords(text) {
267
1354
  }
268
1355
 
269
1356
  // src/schemas/gap-select.ts
270
- import { z as z5 } from "zod/v4";
271
- var GapSelectChoiceSchema = z5.looseObject({
272
- id: z5.string().min(1),
273
- text: z5.string().min(1)
1357
+ import { z as z6 } from "zod/v4";
1358
+ var GapSelectChoiceSchema = z6.looseObject({
1359
+ id: z6.string().min(1),
1360
+ text: z6.string().min(1)
274
1361
  });
275
- var GapSelectBankSchema = z5.looseObject({
276
- id: z5.string().min(1),
277
- choices: z5.array(GapSelectChoiceSchema).min(2)
1362
+ var GapSelectBankSchema = z6.looseObject({
1363
+ id: z6.string().min(1),
1364
+ choices: z6.array(GapSelectChoiceSchema).min(2)
278
1365
  });
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()
1366
+ var GapSelectGapSchema = z6.looseObject({
1367
+ id: z6.string().min(1),
1368
+ choices: z6.array(GapSelectChoiceSchema).min(2).optional(),
1369
+ bankId: z6.string().min(1).optional(),
1370
+ correctChoiceId: z6.string().min(1),
1371
+ feedback: z6.string().optional()
285
1372
  });
286
1373
  function resolveChoices(gap, banks) {
287
1374
  if (gap.choices !== void 0) {
@@ -300,24 +1387,24 @@ function placeholderCounts(passage) {
300
1387
  }
301
1388
  return counts;
302
1389
  }
303
- var GapSelectDataSchema = z5.looseObject({
304
- schemaVersion: z5.literal("1.0"),
305
- type: z5.literal("gap-select"),
306
- id: z5.string().min(1),
307
- title: z5.string().min(1),
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(),
1390
+ var GapSelectDataSchema = z6.looseObject({
1391
+ schemaVersion: z6.literal("1.0"),
1392
+ type: z6.literal("gap-select"),
1393
+ id: z6.string().min(1),
1394
+ title: z6.string().min(1),
1395
+ passage: z6.string().min(1),
1396
+ passageHtml: z6.string().optional(),
1397
+ gaps: z6.array(GapSelectGapSchema).min(1),
1398
+ banks: z6.array(GapSelectBankSchema).optional(),
1399
+ scoringStrategy: z6.enum(["all-or-nothing", "partial"]),
1400
+ presentation: z6.literal("dropdown").optional(),
1401
+ shuffleChoices: z6.boolean().optional(),
315
1402
  media: MediaSchema.optional(),
316
1403
  feedback: FeedbackSchema.optional(),
317
- passThreshold: z5.number().min(0).max(1).optional(),
318
- locale: z5.string().optional(),
319
- learningObjectives: z5.array(z5.string()).optional(),
320
- difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional()
1404
+ passThreshold: z6.number().min(0).max(1).optional(),
1405
+ locale: z6.string().optional(),
1406
+ learningObjectives: z6.array(z6.string()).optional(),
1407
+ difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional()
321
1408
  }).refine(
322
1409
  (data) => {
323
1410
  const ids = (data.banks ?? []).map((bank) => bank.id);
@@ -371,131 +1458,359 @@ var GapSelectDataSchema = z5.looseObject({
371
1458
  );
372
1459
 
373
1460
  // 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)
1461
+ import { z as z7 } from "zod/v4";
1462
+ var WrittenResponseRubricCriterionSchema = z7.looseObject({
1463
+ name: z7.string().min(1),
1464
+ description: z7.string().optional(),
1465
+ weight: z7.number().min(0)
379
1466
  });
380
- var WrittenResponseRubricSchema = z6.looseObject({
381
- label: z6.string().optional(),
382
- criteria: z6.array(WrittenResponseRubricCriterionSchema).min(1)
1467
+ var WrittenResponseRubricSchema = z7.looseObject({
1468
+ label: z7.string().optional(),
1469
+ criteria: z7.array(WrittenResponseRubricCriterionSchema).min(1)
383
1470
  });
384
- var RedactedWrittenResponseRubricCriterionSchema = z6.strictObject({
385
- name: z6.string().min(1),
386
- description: z6.string().optional(),
387
- weight: z6.number().min(0)
1471
+ var RedactedWrittenResponseRubricCriterionSchema = z7.strictObject({
1472
+ name: z7.string().min(1),
1473
+ description: z7.string().optional(),
1474
+ weight: z7.number().min(0)
388
1475
  });
389
- var RedactedWrittenResponseRubricSchema = z6.strictObject({
390
- label: z6.string().optional(),
391
- criteria: z6.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
1476
+ var RedactedWrittenResponseRubricSchema = z7.strictObject({
1477
+ label: z7.string().optional(),
1478
+ criteria: z7.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
392
1479
  });
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),
1480
+ var WrittenResponseDataSchema = z7.looseObject({
1481
+ schemaVersion: z7.literal("1.0"),
1482
+ type: z7.literal("written-response"),
1483
+ id: z7.string().min(1),
1484
+ title: z7.string().min(1),
1485
+ prompt: z7.string(),
1486
+ promptHtml: z7.string().optional(),
1487
+ minWords: z7.number().int().min(0),
1488
+ maxWords: z7.number().int().min(1),
402
1489
  rubric: WrittenResponseRubricSchema.optional(),
403
- languageTarget: z6.string().optional(),
1490
+ languageTarget: z7.string().optional(),
404
1491
  media: MediaSchema.optional(),
405
1492
  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()
1493
+ passThreshold: z7.number().min(0).max(1).optional(),
1494
+ locale: z7.string().optional(),
1495
+ learningObjectives: z7.array(z7.string()).optional(),
1496
+ difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional()
410
1497
  }).refine((data) => data.maxWords >= data.minWords, {
411
1498
  error: "maxWords must be greater than or equal to minWords.",
412
1499
  path: ["maxWords"]
413
1500
  });
414
1501
 
415
1502
  // src/schemas/redacted.ts
416
- import { z as z7 } from "zod/v4";
1503
+ import { z as z8 } from "zod/v4";
417
1504
  var redactedBase = {
418
1505
  /** Marker distinguishing a redacted projection from full activity data. */
419
- redacted: z7.literal(true),
1506
+ redacted: z8.literal(true),
420
1507
  /** Slot identity, carried through redaction so the client and the plan agree. */
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),
1508
+ slotKey: z8.string().min(1).optional(),
1509
+ schemaVersion: z8.literal("1.0"),
1510
+ id: z8.string().min(1),
1511
+ title: z8.string().min(1),
425
1512
  media: RedactedMediaSchema.optional(),
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()
1513
+ passThreshold: z8.number().min(0).max(1).optional(),
1514
+ locale: z8.string().optional(),
1515
+ learningObjectives: z8.array(z8.string()).optional(),
1516
+ difficultyLevel: z8.literal([1, 2, 3, 4, 5]).optional()
430
1517
  };
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()
1518
+ var RedactedMultipleChoiceOptionMediaSchema = z8.strictObject({
1519
+ type: z8.enum(["image", "audio"]),
1520
+ url: z8.string().min(1),
1521
+ alt: z8.string().min(1).optional(),
1522
+ captionsUrl: z8.string().min(1).optional()
436
1523
  });
437
- var RedactedMultipleChoiceOptionSchema = z7.strictObject({
438
- id: z7.string().min(1),
439
- text: z7.string().min(1),
1524
+ var RedactedMultipleChoiceOptionSchema = z8.strictObject({
1525
+ id: z8.string().min(1),
1526
+ text: z8.string().min(1),
440
1527
  media: RedactedMultipleChoiceOptionMediaSchema.optional()
441
1528
  });
442
- var RedactedMultipleChoiceDataSchema = z7.strictObject({
1529
+ var RedactedMultipleChoiceDataSchema = z8.strictObject({
443
1530
  ...redactedBase,
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()
1531
+ type: z8.literal("multiple-choice"),
1532
+ question: z8.string().min(1),
1533
+ questionHtml: z8.string().optional(),
1534
+ mode: z8.enum(["single", "multi"]),
1535
+ options: z8.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
1536
+ shuffle: z8.boolean().optional()
450
1537
  });
451
- var RedactedBlankConfigSchema = z7.strictObject({
452
- id: z7.string().min(1),
453
- hint: z7.string().optional()
1538
+ var RedactedBlankConfigSchema = z8.strictObject({
1539
+ id: z8.string().min(1),
1540
+ hint: z8.string().optional()
454
1541
  });
455
- var RedactedFillInTheBlanksDataSchema = z7.strictObject({
1542
+ var RedactedFillInTheBlanksDataSchema = z8.strictObject({
456
1543
  ...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)
1544
+ type: z8.literal("fill-in-the-blanks"),
1545
+ passage: z8.string().min(1),
1546
+ passageHtml: z8.string().optional(),
1547
+ blanks: z8.array(RedactedBlankConfigSchema).min(1)
461
1548
  });
462
- var RedactedGapSelectChoiceSchema = z7.strictObject({
463
- id: z7.string().min(1),
464
- text: z7.string().min(1)
1549
+ var RedactedGapSelectChoiceSchema = z8.strictObject({
1550
+ id: z8.string().min(1),
1551
+ text: z8.string().min(1)
465
1552
  });
466
- var RedactedGapSelectBankSchema = z7.strictObject({
467
- id: z7.string().min(1),
468
- choices: z7.array(RedactedGapSelectChoiceSchema).min(2)
1553
+ var RedactedGapSelectBankSchema = z8.strictObject({
1554
+ id: z8.string().min(1),
1555
+ choices: z8.array(RedactedGapSelectChoiceSchema).min(2)
469
1556
  });
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()
1557
+ var RedactedGapSelectGapSchema = z8.strictObject({
1558
+ id: z8.string().min(1),
1559
+ choices: z8.array(RedactedGapSelectChoiceSchema).min(2).optional(),
1560
+ bankId: z8.string().min(1).optional()
474
1561
  });
475
- var RedactedGapSelectDataSchema = z7.strictObject({
1562
+ var RedactedGapSelectDataSchema = z8.strictObject({
476
1563
  ...redactedBase,
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()
1564
+ type: z8.literal("gap-select"),
1565
+ passage: z8.string().min(1),
1566
+ passageHtml: z8.string().optional(),
1567
+ gaps: z8.array(RedactedGapSelectGapSchema).min(1),
1568
+ banks: z8.array(RedactedGapSelectBankSchema).optional(),
1569
+ presentation: z8.literal("dropdown").optional(),
1570
+ shuffleChoices: z8.boolean().optional()
484
1571
  });
485
- var RedactedWrittenResponseDataSchema = z7.strictObject({
1572
+ var RedactedWrittenResponseDataSchema = z8.strictObject({
486
1573
  ...redactedBase,
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),
1574
+ type: z8.literal("written-response"),
1575
+ prompt: z8.string(),
1576
+ promptHtml: z8.string().optional(),
1577
+ minWords: z8.number().int().min(0),
1578
+ maxWords: z8.number().int().min(1),
492
1579
  rubric: RedactedWrittenResponseRubricSchema.optional(),
493
- languageTarget: z7.string().optional()
1580
+ languageTarget: z8.string().optional()
1581
+ });
1582
+ var RedactedDictationSlowMediaSchema = z8.strictObject({
1583
+ type: z8.literal("audio"),
1584
+ url: MediaUrlSchema,
1585
+ alt: z8.string().min(1).optional()
1586
+ });
1587
+ var RedactedDictationDataSchema = z8.strictObject({
1588
+ ...redactedBase,
1589
+ type: z8.literal("dictation"),
1590
+ slowMedia: RedactedDictationSlowMediaSchema.optional(),
1591
+ hints: z8.strictObject({ mode: z8.literal("progressive-words") }).optional()
1592
+ }).check((ctx) => {
1593
+ const data = ctx.value;
1594
+ if (data.media?.captionsUrl !== void 0) {
1595
+ ctx.issues.push({
1596
+ code: "custom",
1597
+ input: data.media.captionsUrl,
1598
+ message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
1599
+ path: ["media", "captionsUrl"]
1600
+ });
1601
+ }
1602
+ if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
1603
+ ctx.issues.push({
1604
+ code: "custom",
1605
+ input: data.slowMedia,
1606
+ message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
1607
+ path: ["slowMedia"]
1608
+ });
1609
+ }
1610
+ if (data.slowMedia !== void 0 && data.media === void 0) {
1611
+ ctx.issues.push({
1612
+ code: "custom",
1613
+ input: data.slowMedia,
1614
+ message: "A slow recording accompanies a recording: this payload has no `media`.",
1615
+ path: ["media"]
1616
+ });
1617
+ }
494
1618
  });
495
1619
 
1620
+ // src/scoring/dictation/edit-distance.ts
1621
+ function codePoints(text) {
1622
+ return Array.from(text, (character) => character.codePointAt(0));
1623
+ }
1624
+ function editDistance(a, b) {
1625
+ if (a === b) {
1626
+ return 0;
1627
+ }
1628
+ let left = codePoints(a);
1629
+ let right = codePoints(b);
1630
+ if (left.length < right.length) {
1631
+ [left, right] = [right, left];
1632
+ }
1633
+ if (right.length === 0) {
1634
+ return left.length;
1635
+ }
1636
+ let previous = new Uint32Array(right.length + 1);
1637
+ let current = new Uint32Array(right.length + 1);
1638
+ for (let j = 0; j <= right.length; j += 1) {
1639
+ previous[j] = j;
1640
+ }
1641
+ for (let i = 1; i <= left.length; i += 1) {
1642
+ current[0] = i;
1643
+ const leftPoint = left[i - 1];
1644
+ for (let j = 1; j <= right.length; j += 1) {
1645
+ const deletion = previous[j] + 1;
1646
+ const insertion = current[j - 1] + 1;
1647
+ const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
1648
+ current[j] = Math.min(deletion, insertion, substitution);
1649
+ }
1650
+ [previous, current] = [current, previous];
1651
+ }
1652
+ return previous[right.length];
1653
+ }
1654
+ function similarity(a, b) {
1655
+ const max = Math.max(codePoints(a).length, codePoints(b).length);
1656
+ return max === 0 ? 0 : (max - editDistance(a, b)) / max;
1657
+ }
1658
+
1659
+ // src/scoring/dictation/align.ts
1660
+ function dictationReferenceWords(data) {
1661
+ return rawCandidates(data).map(
1662
+ (candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
1663
+ itemId: `w${index + 1}`,
1664
+ word
1665
+ }))
1666
+ );
1667
+ }
1668
+ function alignDictation(data, text) {
1669
+ const raw = typeof text === "string" ? text : "";
1670
+ const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
1671
+ const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
1672
+ const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
1673
+ const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
1674
+ const candidates = [];
1675
+ rawCandidates(data).forEach((candidate, index) => {
1676
+ const candidateText = normalizeCandidate(candidate, data);
1677
+ if (candidateText !== "") {
1678
+ candidates.push({ index, text: candidateText });
1679
+ }
1680
+ });
1681
+ if (candidates.length === 0) {
1682
+ return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
1683
+ }
1684
+ let chosen = candidates[0];
1685
+ let best = similarity(chosen.text, attempt);
1686
+ for (const candidate of candidates.slice(1)) {
1687
+ const candidateSimilarity = similarity(candidate.text, attempt);
1688
+ if (candidateSimilarity > best) {
1689
+ best = candidateSimilarity;
1690
+ chosen = candidate;
1691
+ }
1692
+ }
1693
+ return {
1694
+ candidateIndex: chosen.index,
1695
+ reference: chosen.text,
1696
+ attempt,
1697
+ truncated,
1698
+ similarity: best,
1699
+ words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
1700
+ };
1701
+ }
1702
+ function rawCandidates(data) {
1703
+ return [
1704
+ data.transcript,
1705
+ ...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
1706
+ ];
1707
+ }
1708
+ function normalizeCandidate(candidate, data) {
1709
+ return typeof candidate === "string" ? cutAt(
1710
+ normalizeDictationText(
1711
+ truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
1712
+ data.tolerance
1713
+ ),
1714
+ DICTATION_MAX_TRANSCRIPT_LENGTH
1715
+ ) : "";
1716
+ }
1717
+ function cutAt(normalized, max) {
1718
+ const cut = truncateCodePoints(normalized, max);
1719
+ return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
1720
+ }
1721
+ function tokensOf(normalized) {
1722
+ return normalized === "" ? [] : normalized.split(" ");
1723
+ }
1724
+ var STEP = {
1725
+ /** A reference element paired with an attempt element — equal or substituted. */
1726
+ pair: 0,
1727
+ /** A reference element the attempt lacks. */
1728
+ missing: 1,
1729
+ /** An attempt element the reference lacks. */
1730
+ extra: 2
1731
+ };
1732
+ function alignSequences(reference, attempt) {
1733
+ const rows = reference.length;
1734
+ const columns = attempt.length;
1735
+ const width = columns + 1;
1736
+ const steps = new Uint8Array((rows + 1) * width);
1737
+ let previous = new Uint32Array(width);
1738
+ let current = new Uint32Array(width);
1739
+ for (let j2 = 0; j2 <= columns; j2 += 1) {
1740
+ previous[j2] = j2;
1741
+ steps[j2] = STEP.extra;
1742
+ }
1743
+ for (let i2 = 1; i2 <= rows; i2 += 1) {
1744
+ current[0] = i2;
1745
+ steps[i2 * width] = STEP.missing;
1746
+ const referenceItem = reference[i2 - 1];
1747
+ for (let j2 = 1; j2 <= columns; j2 += 1) {
1748
+ const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
1749
+ const missing = previous[j2] + 1;
1750
+ const extra = current[j2 - 1] + 1;
1751
+ const cost = Math.min(paired, missing, extra);
1752
+ current[j2] = cost;
1753
+ steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
1754
+ }
1755
+ [previous, current] = [current, previous];
1756
+ }
1757
+ const path = [];
1758
+ let i = rows;
1759
+ let j = columns;
1760
+ while (i > 0 || j > 0) {
1761
+ const step = steps[i * width + j];
1762
+ if (step === STEP.pair) {
1763
+ i -= 1;
1764
+ j -= 1;
1765
+ } else if (step === STEP.missing) {
1766
+ i -= 1;
1767
+ } else {
1768
+ j -= 1;
1769
+ }
1770
+ path.push({ step, referenceIndex: i, attemptIndex: j });
1771
+ }
1772
+ return path.reverse();
1773
+ }
1774
+ function alignWords(reference, attempt) {
1775
+ const words = [];
1776
+ let ordinal = 0;
1777
+ for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
1778
+ if (step === STEP.extra) {
1779
+ words.push({
1780
+ reference: "",
1781
+ attempt: attempt[attemptIndex],
1782
+ similarity: 0,
1783
+ status: "extra"
1784
+ });
1785
+ continue;
1786
+ }
1787
+ ordinal += 1;
1788
+ const referenceWord = reference[referenceIndex];
1789
+ if (step === STEP.missing) {
1790
+ words.push({
1791
+ itemId: `w${ordinal}`,
1792
+ reference: referenceWord,
1793
+ attempt: "",
1794
+ similarity: 0,
1795
+ status: "missing"
1796
+ });
1797
+ continue;
1798
+ }
1799
+ const attemptWord = attempt[attemptIndex];
1800
+ words.push({
1801
+ itemId: `w${ordinal}`,
1802
+ reference: referenceWord,
1803
+ attempt: attemptWord,
1804
+ similarity: similarity(referenceWord, attemptWord),
1805
+ status: referenceWord === attemptWord ? "correct" : "incorrect"
1806
+ });
1807
+ }
1808
+ return words;
1809
+ }
1810
+
496
1811
  // src/scoring/text-match.ts
497
1812
  var COMBINING_MARKS_RE = /\p{M}/gu;
498
- var PUNCTUATION_RE = /\p{P}/gu;
1813
+ var PUNCTUATION_RE2 = /\p{P}/gu;
499
1814
  function applyBaseline(value, policy) {
500
1815
  let result = value;
501
1816
  if (policy.trim !== false) {
@@ -512,7 +1827,7 @@ function applyNormalization(value, policy) {
512
1827
  result = result.normalize(policy.normalize);
513
1828
  }
514
1829
  if (policy.ignorePunctuation === true) {
515
- result = result.replace(PUNCTUATION_RE, "");
1830
+ result = result.replace(PUNCTUATION_RE2, "");
516
1831
  }
517
1832
  if (policy.collapseInnerWhitespace === true) {
518
1833
  result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
@@ -700,7 +2015,29 @@ var DRAFT_ISSUE_SEVERITY = {
700
2015
  wr_criterion_weight_required: "incomplete",
701
2016
  wr_criterion_weight_invalid: "invalid",
702
2017
  wr_rubric_weights_zero: "incomplete",
703
- wr_rubric_weights_too_large: "invalid"
2018
+ wr_rubric_weights_too_large: "invalid",
2019
+ // dictation
2020
+ dc_transcript_required: "incomplete",
2021
+ dc_transcript_unscorable: "invalid",
2022
+ dc_transcript_too_long: "invalid",
2023
+ dc_accepted_transcript_empty: "incomplete",
2024
+ dc_accepted_transcript_too_long: "invalid",
2025
+ dc_accepted_transcript_duplicate: "invalid",
2026
+ dc_accepted_transcripts_too_many: "invalid",
2027
+ dc_media_kind: "invalid",
2028
+ dc_captions_not_allowed: "invalid",
2029
+ dc_slow_media_kind: "invalid",
2030
+ dc_slow_media_playback: "invalid",
2031
+ dc_slow_media_unbudgeted: "invalid",
2032
+ dc_slow_media_same_url: "invalid",
2033
+ dc_slow_media_without_media: "incomplete",
2034
+ dc_transcript_revealed: "invalid",
2035
+ dc_shown_text_too_long: "invalid",
2036
+ dc_hints_mode_required: "incomplete",
2037
+ dc_hints_mode_invalid: "invalid",
2038
+ dc_equivalence_from_required: "incomplete",
2039
+ dc_equivalence_to_required: "incomplete",
2040
+ dc_tolerance_invalid: "invalid"
704
2041
  };
705
2042
  function issue(code, path, message) {
706
2043
  return { path: path.map(String), message, code, severity: DRAFT_ISSUE_SEVERITY[code] };
@@ -879,6 +2216,355 @@ function covers(reported, failed) {
879
2216
  }
880
2217
  return failed.every((segment, index) => reported[index] === segment);
881
2218
  }
2219
+ function pathKey(path) {
2220
+ return path.length === 0 ? "" : JSON.stringify(path.map(String));
2221
+ }
2222
+ function coveredPathsOf(reported) {
2223
+ const covered = /* @__PURE__ */ new Set();
2224
+ for (const known of reported) {
2225
+ if (known.path.length === 0) {
2226
+ covered.add("");
2227
+ }
2228
+ for (let length = 1; length <= known.path.length; length += 1) {
2229
+ covered.add(pathKey(known.path.slice(0, length)));
2230
+ }
2231
+ }
2232
+ return covered;
2233
+ }
2234
+
2235
+ // src/authoring/dictation.ts
2236
+ var dictationAuthoring = {
2237
+ createDraft: ({ newId }) => ({
2238
+ schemaVersion: "1.0",
2239
+ type: "dictation",
2240
+ id: newId(),
2241
+ title: "",
2242
+ transcript: ""
2243
+ }),
2244
+ checkDraft: checkDictationDraft
2245
+ };
2246
+ function checkDictationDraft(draft) {
2247
+ const issues = checkIdentity(draft, "dictation");
2248
+ const tolerance = isRecord(draft.tolerance) ? draft.tolerance : void 0;
2249
+ const normalizer = dictationNormalizer(tolerance);
2250
+ issues.push(...checkTranscript(draft.transcript, normalizer));
2251
+ issues.push(...checkAcceptedTranscripts(draft, normalizer));
2252
+ issues.push(...checkRecording(draft.media));
2253
+ issues.push(...checkSlowRecording(draft));
2254
+ issues.push(...checkRevealed(draft, normalizer));
2255
+ issues.push(...checkHints(draft.hints));
2256
+ issues.push(...checkTolerance(draft.tolerance));
2257
+ issues.push(...checkSharedOptional(draft));
2258
+ return issues;
2259
+ }
2260
+ function isUnwrittenTranscript(value) {
2261
+ if (typeof value === "string" && isLongerThan(value, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
2262
+ return false;
2263
+ }
2264
+ return isUnwritten(value) || typeof value === "string" && isBlankBeforeRules(value);
2265
+ }
2266
+ function acceptedTranscriptsRead(accepted) {
2267
+ return accepted.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS);
2268
+ }
2269
+ function checkTranscript(transcript, normalizer) {
2270
+ if (isUnwrittenTranscript(transcript)) {
2271
+ return [issue("dc_transcript_required", ["transcript"], "Write the transcript.")];
2272
+ }
2273
+ if (typeof transcript !== "string") {
2274
+ return [];
2275
+ }
2276
+ const measured = normalizer.measure(transcript);
2277
+ if (measured.tooLong) {
2278
+ return [
2279
+ issue(
2280
+ "dc_transcript_too_long",
2281
+ ["transcript"],
2282
+ `The transcript is longer than ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters before or after its equivalences are applied, or they grow it past ${WORKING_LENGTH} characters on the way. A dictation is a sentence or two.`
2283
+ )
2284
+ ];
2285
+ }
2286
+ if (measured.normalized === "") {
2287
+ return [
2288
+ issue(
2289
+ "dc_transcript_unscorable",
2290
+ ["transcript"],
2291
+ "The transcript has nothing to type: once case, punctuation and spacing are ignored, nothing is left."
2292
+ )
2293
+ ];
2294
+ }
2295
+ return [];
2296
+ }
2297
+ function checkAcceptedTranscripts(draft, normalizer) {
2298
+ const accepted = draft.acceptedTranscripts;
2299
+ if (!Array.isArray(accepted)) {
2300
+ return [];
2301
+ }
2302
+ const issues = [];
2303
+ if (accepted.length > DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
2304
+ issues.push(
2305
+ issue(
2306
+ "dc_accepted_transcripts_too_many",
2307
+ ["acceptedTranscripts"],
2308
+ `At most ${DICTATION_MAX_ACCEPTED_TRANSCRIPTS} accepted transcripts. Prefer an equivalence for a word that varies.`
2309
+ )
2310
+ );
2311
+ }
2312
+ for (const [index, text] of accepted.entries()) {
2313
+ if (typeof text !== "string") {
2314
+ continue;
2315
+ }
2316
+ if (index >= DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
2317
+ if (text === "") {
2318
+ issues.push(acceptedTranscriptEmpty(index));
2319
+ }
2320
+ continue;
2321
+ }
2322
+ const measured = normalizer.measure(text);
2323
+ if (measured.tooLong) {
2324
+ issues.push(
2325
+ issue(
2326
+ "dc_accepted_transcript_too_long",
2327
+ ["acceptedTranscripts", index],
2328
+ `Accepted transcript ${index + 1} is longer than ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters before or after its equivalences are applied, or they grow it past ${WORKING_LENGTH} characters on the way.`
2329
+ )
2330
+ );
2331
+ } else if (measured.normalized === "") {
2332
+ issues.push(acceptedTranscriptEmpty(index));
2333
+ }
2334
+ }
2335
+ const candidates = [draft.transcript, ...acceptedTranscriptsRead(accepted)].filter((text) => typeof text === "string").map((text) => normalizer.measure(text).normalized).filter((text) => text !== null && text !== "");
2336
+ if (new Set(candidates).size !== candidates.length) {
2337
+ issues.push(
2338
+ issue(
2339
+ "dc_accepted_transcript_duplicate",
2340
+ ["acceptedTranscripts"],
2341
+ "An accepted transcript repeats the transcript, or another accepted transcript, once case, punctuation and spacing are ignored. Remove it."
2342
+ )
2343
+ );
2344
+ }
2345
+ return issues;
2346
+ }
2347
+ function acceptedTranscriptEmpty(index) {
2348
+ return issue(
2349
+ "dc_accepted_transcript_empty",
2350
+ ["acceptedTranscripts", index],
2351
+ `Accepted transcript ${index + 1} has nothing to type. Fill it in or remove it.`
2352
+ );
2353
+ }
2354
+ function checkRecording(media) {
2355
+ if (!isRecord(media)) {
2356
+ return [];
2357
+ }
2358
+ const issues = [];
2359
+ const kind = media.type;
2360
+ if (!isUnwritten(kind) && kind !== "audio") {
2361
+ issues.push(
2362
+ issue(
2363
+ "dc_media_kind",
2364
+ ["media", "type"],
2365
+ "A dictation is dictated: its recording must be audio."
2366
+ )
2367
+ );
2368
+ }
2369
+ if (!isUnset(media.captionsUrl)) {
2370
+ issues.push(
2371
+ issue("dc_captions_not_allowed", ["media", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
2372
+ );
2373
+ }
2374
+ return issues;
2375
+ }
2376
+ var CAPTIONS_ARE_THE_ANSWER = "A dictation recording cannot carry captions: the captions are the answer. Remove them, and offer an accessible alternative as a different item.";
2377
+ function checkSlowRecording(draft) {
2378
+ const slow = draft.slowMedia;
2379
+ if (!isRecord(slow)) {
2380
+ return [];
2381
+ }
2382
+ const issues = [];
2383
+ const kind = slow.type;
2384
+ const wrongKind = !isUnwritten(kind) && kind !== "audio";
2385
+ if (wrongKind) {
2386
+ issues.push(
2387
+ issue("dc_slow_media_kind", ["slowMedia", "type"], "The slow recording must be audio.")
2388
+ );
2389
+ }
2390
+ issues.push(
2391
+ ...checkMedia(
2392
+ wrongKind ? { ...slow, type: "audio" } : slow,
2393
+ ["slowMedia"],
2394
+ DictationSlowMediaSchema
2395
+ )
2396
+ );
2397
+ if (!isUnset(slow.captionsUrl)) {
2398
+ issues.push(
2399
+ issue("dc_captions_not_allowed", ["slowMedia", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
2400
+ );
2401
+ }
2402
+ if (slow.playback !== void 0) {
2403
+ issues.push(
2404
+ issue(
2405
+ "dc_slow_media_playback",
2406
+ ["slowMedia", "playback"],
2407
+ "The slow recording has no playback policy of its own. Put the policy on the recording; the slow recording follows it, except the play budget."
2408
+ )
2409
+ );
2410
+ }
2411
+ const media = draft.media;
2412
+ if (isUnset(media)) {
2413
+ issues.push(
2414
+ issue(
2415
+ "dc_slow_media_without_media",
2416
+ ["media"],
2417
+ "A slow recording accompanies a recording. Add the recording first."
2418
+ )
2419
+ );
2420
+ return issues;
2421
+ }
2422
+ if (!isRecord(media)) {
2423
+ return issues;
2424
+ }
2425
+ if (isRecord(media.playback) && media.playback.maxPlays !== void 0) {
2426
+ issues.push(
2427
+ issue(
2428
+ "dc_slow_media_unbudgeted",
2429
+ ["slowMedia"],
2430
+ "A play budget on the recording cannot coexist with a slow recording that has none. Remove the slow recording, or remove the play limit."
2431
+ )
2432
+ );
2433
+ }
2434
+ if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) {
2435
+ issues.push(
2436
+ issue(
2437
+ "dc_slow_media_same_url",
2438
+ ["slowMedia", "url"],
2439
+ "The slow recording is the same file as the recording. Point it at the slower render."
2440
+ )
2441
+ );
2442
+ }
2443
+ return issues;
2444
+ }
2445
+ function checkRevealed(draft, normalizer) {
2446
+ const candidates = [
2447
+ draft.transcript,
2448
+ ...Array.isArray(draft.acceptedTranscripts) ? acceptedTranscriptsRead(draft.acceptedTranscripts) : []
2449
+ ].filter((text) => typeof text === "string").map((text) => normalizer.measure(text));
2450
+ const shown = [
2451
+ [["title"], draft.title],
2452
+ [["media", "alt"], isRecord(draft.media) ? draft.media.alt : void 0],
2453
+ [["slowMedia", "alt"], isRecord(draft.slowMedia) ? draft.slowMedia.alt : void 0]
2454
+ ];
2455
+ const issues = [];
2456
+ for (const [path, text] of shown) {
2457
+ if (typeof text !== "string") {
2458
+ continue;
2459
+ }
2460
+ const { tooLong, forms } = normalizer.shown(text);
2461
+ if (tooLong) {
2462
+ issues.push(
2463
+ issue(
2464
+ "dc_shown_text_too_long",
2465
+ path,
2466
+ `${path[0] === "title" ? "The title" : "The description"} is too long to be checked for the transcript: at most ${WORKING_LENGTH} characters before and after the equivalences are applied, and they may not grow it past that on the way.`
2467
+ )
2468
+ );
2469
+ } else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
2470
+ issues.push(
2471
+ issue(
2472
+ "dc_transcript_revealed",
2473
+ path,
2474
+ path[0] === "title" ? 'The title gives the answer away: it contains the transcript. Name the exercise instead ("Listen and type the sentence").' : 'The description gives the answer away: it contains the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
2475
+ )
2476
+ );
2477
+ }
2478
+ }
2479
+ return issues;
2480
+ }
2481
+ function checkHints(hints) {
2482
+ if (!isRecord(hints)) {
2483
+ return [];
2484
+ }
2485
+ const mode = hints.mode;
2486
+ if (isUnwritten(mode)) {
2487
+ return [issue("dc_hints_mode_required", ["hints", "mode"], "Choose how hints are given.")];
2488
+ }
2489
+ if (mode !== "progressive-words") {
2490
+ return [
2491
+ issue(
2492
+ "dc_hints_mode_invalid",
2493
+ ["hints", "mode"],
2494
+ 'The only hint mode is "progressive-words". Leave hints out unless you mean to offer them.'
2495
+ )
2496
+ ];
2497
+ }
2498
+ return [];
2499
+ }
2500
+ function checkTolerance(tolerance) {
2501
+ if (isUnset(tolerance)) {
2502
+ return [];
2503
+ }
2504
+ if (!isRecord(tolerance)) {
2505
+ return [
2506
+ issue(
2507
+ "dc_tolerance_invalid",
2508
+ ["tolerance"],
2509
+ "The tolerances are an object: `{ equivalences: [{ from, to }] }`."
2510
+ )
2511
+ ];
2512
+ }
2513
+ const issues = [];
2514
+ const rules = tolerance.equivalences;
2515
+ if (Array.isArray(rules)) {
2516
+ if (rules.length > DICTATION_MAX_EQUIVALENCES) {
2517
+ issues.push(
2518
+ issue(
2519
+ "dc_tolerance_invalid",
2520
+ ["tolerance", "equivalences"],
2521
+ `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
2522
+ )
2523
+ );
2524
+ }
2525
+ for (const [index, rule] of rules.entries()) {
2526
+ if (!isRecord(rule)) {
2527
+ continue;
2528
+ }
2529
+ const path = ["tolerance", "equivalences", index];
2530
+ const from = rule.from;
2531
+ if (isUnwritten(from) || typeof from === "string" && preStripNormalize(from) === "") {
2532
+ issues.push(
2533
+ issue(
2534
+ "dc_equivalence_from_required",
2535
+ [...path, "from"],
2536
+ `Say what rule ${index + 1} rewrites: a word, a phrase or a symbol.`
2537
+ )
2538
+ );
2539
+ }
2540
+ const to = rule.to;
2541
+ if (isUnwritten(to) || typeof to === "string" && normalizeDictationText(to) === "") {
2542
+ issues.push(
2543
+ issue(
2544
+ "dc_equivalence_to_required",
2545
+ [...path, "to"],
2546
+ `Say what rule ${index + 1} rewrites to. A rule cannot delete a word or leave only punctuation.`
2547
+ )
2548
+ );
2549
+ }
2550
+ }
2551
+ }
2552
+ const parsed = DictationToleranceSchema.safeParse(tolerance, { reportInput: true });
2553
+ if (!parsed.success) {
2554
+ const covered = coveredPathsOf(issues);
2555
+ for (const schemaIssue of parsed.error.issues) {
2556
+ const path = ["tolerance", ...schemaIssue.path.map(String)];
2557
+ if (refusesEmpty(tolerance, schemaIssue) || covered.has(pathKey(path))) {
2558
+ continue;
2559
+ }
2560
+ issues.push(issue("dc_tolerance_invalid", path, schemaIssue.message));
2561
+ for (const key of coveredPathsOf([{ path }])) {
2562
+ covered.add(key);
2563
+ }
2564
+ }
2565
+ }
2566
+ return issues;
2567
+ }
882
2568
 
883
2569
  // src/authoring/fill-in-the-blanks.ts
884
2570
  var fillInTheBlanksAuthoring = {
@@ -1633,6 +3319,27 @@ function checkCriteria(criteria) {
1633
3319
  return issues;
1634
3320
  }
1635
3321
 
3322
+ // src/scoring/activity-scorers/dictation.ts
3323
+ function scoreDictation(data, response) {
3324
+ const alignment = alignDictation(data, typeof response?.text === "string" ? response.text : "");
3325
+ const details = [];
3326
+ for (const word of alignment.words) {
3327
+ if (word.status === "extra") {
3328
+ continue;
3329
+ }
3330
+ details.push({
3331
+ itemId: word.itemId,
3332
+ correct: word.status === "correct",
3333
+ outcome: word.status === "correct" ? "correct" : word.status === "incorrect" ? "incorrect" : "incorrect-omission",
3334
+ learnerResponse: word.attempt,
3335
+ correctResponse: word.reference,
3336
+ score: word.similarity,
3337
+ weight: 1
3338
+ });
3339
+ }
3340
+ return { score: alignment.similarity, maxScore: 1, feedback: null, details };
3341
+ }
3342
+
1636
3343
  // src/scoring/strategies/all-or-nothing.ts
1637
3344
  function allOrNothingStrategy(correctItems) {
1638
3345
  return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
@@ -1860,6 +3567,15 @@ var FILL_IN_THE_BLANKS_FIELD_POLICY = {
1860
3567
  feedback: "answer-key"
1861
3568
  }
1862
3569
  };
3570
+ var DICTATION_FIELD_POLICY = {
3571
+ ...SHARED_PUBLIC_FIELDS,
3572
+ transcript: "answer-key",
3573
+ acceptedTranscripts: "answer-key",
3574
+ slowMedia: { type: "public", url: "public", alt: "public" },
3575
+ hints: { mode: "public" },
3576
+ tolerance: "answer-key",
3577
+ feedback: FEEDBACK_FIELD_POLICY
3578
+ };
1863
3579
  var RUBRIC_FIELD_POLICY = {
1864
3580
  label: "public",
1865
3581
  // Applies to every element of the array.
@@ -1973,25 +3689,63 @@ var gapSelectType = defineActivityType({
1973
3689
  interactions: ["gap-selected", "submitted"],
1974
3690
  authoring: gapSelectAuthoring
1975
3691
  });
3692
+ var dictationType = defineActivityType({
3693
+ type: "dictation",
3694
+ schema: DictationDataSchema,
3695
+ scoring: { kind: "sync", score: scoreDictation },
3696
+ // Whitespace-only or punctuation-only text is no answer. Without the
3697
+ // tolerance, which this signature cannot see: the one gap is an attempt made
3698
+ // ONLY of a symbol an equivalence names, which reads as unanswered here and
3699
+ // is scored as the rewritten word — documented in the authoring guide. The
3700
+ // reverse cannot happen: a rule inserts words, never nothing, and the text is
3701
+ // cut where the scorer cuts, so text past the cap never counts as an answer.
3702
+ isAnswered: (response) => normalizeDictationText(
3703
+ truncateCodePoints(
3704
+ typeof response?.text === "string" ? response.text : "",
3705
+ DICTATION_MAX_TEXT_LENGTH
3706
+ )
3707
+ ) !== "",
3708
+ fieldPolicy: DICTATION_FIELD_POLICY,
3709
+ redactedSchema: RedactedDictationDataSchema,
3710
+ interop: {
3711
+ xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
3712
+ // `fill-in`, as for fill-in-the-blanks: a typed answer with a key pattern.
3713
+ // `long-fill-in` is what written-response uses for free text nothing scores.
3714
+ xapiInteractionType: "fill-in",
3715
+ correctResponsesPattern: (data) => [data.transcript, ...data.acceptedTranscripts ?? []]
3716
+ },
3717
+ interactions: ["text-changed", "hint-requested", "submitted"],
3718
+ authoring: dictationAuthoring
3719
+ });
1976
3720
  registerActivityType(multipleChoiceType);
1977
3721
  registerActivityType(fillInTheBlanksType);
1978
3722
  registerActivityType(writtenResponseType);
1979
3723
  registerActivityType(gapSelectType);
3724
+ registerActivityType(dictationType);
1980
3725
 
1981
3726
  export {
1982
3727
  ActivitySchemaError,
1983
3728
  UnknownActivityTypeError,
1984
3729
  RedactedScoringError,
1985
3730
  DeferredScoringError,
3731
+ withValidationScope,
3732
+ DICTATION_MAX_TRANSCRIPT_LENGTH,
3733
+ DICTATION_MAX_TEXT_LENGTH,
3734
+ DICTATION_MAX_EQUIVALENCE_LENGTH,
3735
+ DICTATION_MAX_EQUIVALENCES,
3736
+ DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
1986
3737
  FeedbackSchema,
1987
3738
  MediaUrlSchema,
1988
3739
  NativeControlHintSchema,
1989
3740
  MediaPlaybackSchema,
1990
3741
  MediaSchema,
1991
3742
  RedactedMediaSchema,
1992
- TextMatchPolicySchema,
1993
- BlankConfigSchema,
1994
- FillInTheBlanksDataSchema,
3743
+ DictationSlowMediaSchema,
3744
+ DictationEquivalenceSchema,
3745
+ DictationToleranceSchema,
3746
+ DictationDataSchema,
3747
+ groupCaptionsRevealDictation,
3748
+ GROUP_CAPTIONS_REVEAL_DICTATION,
1995
3749
  DRAFT_ISSUE_SEVERITY,
1996
3750
  issue,
1997
3751
  isRecord,
@@ -2001,6 +3755,11 @@ export {
2001
3755
  isMissingId,
2002
3756
  checkMedia,
2003
3757
  covers,
3758
+ pathKey,
3759
+ coveredPathsOf,
3760
+ TextMatchPolicySchema,
3761
+ BlankConfigSchema,
3762
+ FillInTheBlanksDataSchema,
2004
3763
  MultipleChoiceOptionMediaSchema,
2005
3764
  MultipleChoiceOptionSchema,
2006
3765
  MultipleChoiceDataSchema,
@@ -2022,6 +3781,12 @@ export {
2022
3781
  RedactedGapSelectGapSchema,
2023
3782
  RedactedGapSelectDataSchema,
2024
3783
  RedactedWrittenResponseDataSchema,
3784
+ RedactedDictationSlowMediaSchema,
3785
+ RedactedDictationDataSchema,
3786
+ dictationReferenceWords,
3787
+ alignDictation,
3788
+ STEP,
3789
+ alignSequences,
2025
3790
  levenshteinDistance,
2026
3791
  matchText,
2027
3792
  defineActivityType,
@@ -2031,6 +3796,8 @@ export {
2031
3796
  MEDIA_FIELD_POLICY,
2032
3797
  multipleChoiceType,
2033
3798
  fillInTheBlanksType,
2034
- writtenResponseType
3799
+ writtenResponseType,
3800
+ gapSelectType,
3801
+ dictationType
2035
3802
  };
2036
- //# sourceMappingURL=chunk-FVN37GIH.js.map
3803
+ //# sourceMappingURL=chunk-XKKKWKRH.js.map