@intellectif/lk-core 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +36 -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-BefQ8FAS.d.cts} +192 -15
  21. package/dist/{index-CRKtWP5G.d.ts → index-BiIvdASA.d.ts} +192 -15
  22. package/dist/index.cjs +132 -74
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +19 -6
  25. package/dist/index.d.ts +19 -6
  26. package/dist/index.js +64 -6
  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
 
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 = _optionalChain([previous, 'optionalAccess', _2 => _2.codePointAt, 'call', _3 => _3(0)]);
523
+ if (point === MONGOLIAN_VOWEL_SEPARATOR) {
524
+ return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(_optionalChain([points, 'access', _4 => _4[index + 1], 'optionalAccess', _5 => _5.codePointAt, 'call', _6 => _6(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 _nullishCoalesce(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 = _optionalChain([tolerance, 'optionalAccess', _7 => _7.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 = _optionalChain([rule, 'optionalAccess', _8 => _8.from]);
696
+ const rawTo = _optionalChain([rule, 'optionalAccess', _9 => _9.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 = _optionalChain([memo, 'optionalAccess', _10 => _10.get, 'call', _11 => _11(text)]);
766
+ if (result === void 0) {
767
+ result = bounded(preStripNormalize(text));
768
+ _optionalChain([memo, 'optionalAccess', _12 => _12.set, 'call', _13 => _13(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 = _optionalChain([memo, 'optionalAccess', _14 => _14.get, 'call', _15 => _15(compiled.signature)]);
781
+ const remembered = _optionalChain([known, 'optionalAccess', _16 => _16.get, 'call', _17 => _17(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 = (_nullishCoalesce(_optionalChain([source, 'access', _18 => _18.match, 'call', _19 => _19(UNSPACED_LETTERS_RE), 'optionalAccess', _20 => _20.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 = _optionalChain([memo, 'optionalAccess', _21 => _21.get, 'call', _22 => _22(forms), 'optionalAccess', _23 => _23.get, 'call', _24 => _24(candidate)]);
883
+ if (known !== void 0) {
884
+ return known;
885
+ }
886
+ const answer = searchReveal(forms, candidate);
887
+ if (memo !== void 0) {
888
+ const byCandidate = _nullishCoalesce(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 = _nullishCoalesce(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
  var _v4 = require('zod/v4');
43
913
  var FeedbackSchema = _v4.z.looseObject({
@@ -98,22 +968,22 @@ var MediaSchema = _v4.z.looseObject({
98
968
  error: "playback policy is supported on audio media only. An embed is a provider iframe the SDK cannot control at all (it has no reliable JS API for a third-party player); an image has nothing to play; video is deliberately deferred, because a video transport must also own fullscreen and Picture-in-Picture and the SDK will not pretend to govern those yet.",
99
969
  path: ["playback"]
100
970
  }).refine(
101
- (m) => _optionalChain([m, 'access', _2 => _2.playback, 'optionalAccess', _3 => _3.controls]) !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
971
+ (m) => _optionalChain([m, 'access', _25 => _25.playback, 'optionalAccess', _26 => _26.controls]) !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
102
972
  {
103
973
  error: 'controls: "native" cannot carry maxPlays, seek: "none" or rate: "fixed". The browser\'s own bar keeps its play button enabled after a budget is spent, and keeps a scrubber that would move and then silently snap back \u2014 a control that looks operable and does nothing (WCAG 3.2.2 / 4.1.3). Omit `controls` and the SDK transport is used automatically, or use nativeControlHints for a cosmetic hint.',
104
974
  path: ["playback", "controls"]
105
975
  }
106
- ).refine((m) => _optionalChain([m, 'access', _4 => _4.playback, 'optionalAccess', _5 => _5.maxPlays]) === void 0 || m.playback.seek !== "allow", {
976
+ ).refine((m) => _optionalChain([m, 'access', _27 => _27.playback, 'optionalAccess', _28 => _28.maxPlays]) === void 0 || m.playback.seek !== "allow", {
107
977
  error: 'maxPlays cannot be combined with seek: "allow". A play is consumed when playback starts from somewhere other than where it stopped, so scrubbing back mid-play would replay the whole recording without spending anything. Omit `seek` \u2014 it resolves to "none" under a budget.',
108
978
  path: ["playback", "seek"]
109
979
  }).refine(
110
- (m) => _optionalChain([m, 'access', _6 => _6.playback, 'optionalAccess', _7 => _7.controls]) !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
980
+ (m) => _optionalChain([m, 'access', _29 => _29.playback, 'optionalAccess', _30 => _30.controls]) !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
111
981
  {
112
982
  error: 'controls: "minimal" with nothing to enforce trades the browser\'s localized, familiar control bar for the SDK\'s, and buys nothing. Set maxPlays, seek: "none" or rate: "fixed", or omit `controls`.',
113
983
  path: ["playback", "controls"]
114
984
  }
115
985
  ).refine(
116
- (m) => _optionalChain([m, 'access', _8 => _8.playback, 'optionalAccess', _9 => _9.nativeControlHints]) === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
986
+ (m) => _optionalChain([m, 'access', _31 => _31.playback, 'optionalAccess', _32 => _32.nativeControlHints]) === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
117
987
  {
118
988
  error: "nativeControlHints only affects the browser's own control bar, and an enforcing policy replaces that bar with the SDK transport \u2014 so the hints would be silently inert. Use them on an otherwise unrestricted recording, or drop them.",
119
989
  path: ["playback", "nativeControlHints"]
@@ -136,6 +1006,223 @@ var RedactedMediaSchema = _v4.z.strictObject({
136
1006
  path: ["url"]
137
1007
  });
138
1008
 
1009
+ // src/schemas/dictation.ts
1010
+
1011
+ var DictationSlowMediaSchema = _v4.z.looseObject({
1012
+ type: _v4.z.literal("audio"),
1013
+ url: MediaUrlSchema,
1014
+ alt: _v4.z.string().min(1).optional()
1015
+ });
1016
+ var RuleTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1017
+ var TranscriptTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1018
+ var DictationEquivalenceSchema = _v4.z.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 = _v4.z.looseObject({
1029
+ equivalences: _v4.z.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
1030
+ error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
1031
+ }).optional()
1032
+ });
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, ..._nullishCoalesce(data.acceptedTranscripts, () => ( []))];
1047
+ }
1048
+ function isEmpty(measured) {
1049
+ return measured.normalized === "";
1050
+ }
1051
+ function checkEvenAfterIssues(check) {
1052
+ const guard = new _v4.z.core.$ZodCheck({ check: "custom", when: () => true });
1053
+ guard._zod.check = check;
1054
+ return guard;
1055
+ }
1056
+ var DictationDataShape = _v4.z.looseObject({
1057
+ schemaVersion: _v4.z.literal("1.0"),
1058
+ type: _v4.z.literal("dictation"),
1059
+ id: _v4.z.string().min(1),
1060
+ title: _v4.z.string().min(1),
1061
+ transcript: TranscriptTextSchema,
1062
+ acceptedTranscripts: _v4.z.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1063
+ media: MediaSchema.optional(),
1064
+ slowMedia: DictationSlowMediaSchema.optional(),
1065
+ hints: _v4.z.looseObject({ mode: _v4.z.literal("progressive-words") }).optional(),
1066
+ tolerance: DictationToleranceSchema.optional(),
1067
+ feedback: FeedbackSchema.optional(),
1068
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
1069
+ locale: _v4.z.string().optional(),
1070
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1071
+ difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional()
1072
+ });
1073
+ var DictationDataSchema = DictationDataShape.check(
1074
+ checkEvenAfterIssues((ctx) => {
1075
+ if (ctx.issues.some((issue2) => (_nullishCoalesce(_optionalChain([issue2, 'access', _33 => _33.path, 'optionalAccess', _34 => _34.length]), () => ( 0))) === 0)) {
1076
+ return;
1077
+ }
1078
+ const data = ctx.value;
1079
+ const refused = new Set(ctx.issues.map((issue2) => String(_optionalChain([issue2, 'access', _35 => _35.path, 'optionalAccess', _36 => _36[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 = _nullishCoalesce(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 && _optionalChain([data, 'access', _37 => _37.slowMedia, 'optionalAccess', _38 => _38.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 (_optionalChain([data, 'access', _39 => _39.media, 'optionalAccess', _40 => _40.playback, 'optionalAccess', _41 => _41.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 _optionalChain([data, 'access', _42 => _42.media, 'optionalAccess', _43 => _43.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 && !(_nullishCoalesce(_optionalChain([data, 'access', _44 => _44.tolerance, 'optionalAccess', _45 => _45.equivalences]), () => ( []))).every(
1163
+ (rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
1164
+ )) {
1165
+ report(
1166
+ ["tolerance", "equivalences"],
1167
+ _optionalChain([data, 'access', _46 => _46.tolerance, 'optionalAccess', _47 => _47.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 (_optionalChain([recording, 'optionalAccess', _48 => _48.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"], _optionalChain([data, 'access', _49 => _49.media, 'optionalAccess', _50 => _50.alt])],
1189
+ [["slowMedia", "alt"], _optionalChain([data, 'access', _51 => _51.slowMedia, 'optionalAccess', _52 => _52.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
+
139
1226
  // src/schemas/fill-in-the-blanks.ts
140
1227
 
141
1228
  var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
@@ -290,7 +1377,7 @@ function resolveChoices(gap, banks) {
290
1377
  if (gap.bankId === void 0) {
291
1378
  return void 0;
292
1379
  }
293
- return _optionalChain([banks, 'access', _10 => _10.find, 'call', _11 => _11((bank) => bank.id === gap.bankId), 'optionalAccess', _12 => _12.choices]);
1380
+ return _optionalChain([banks, 'access', _53 => _53.find, 'call', _54 => _54((bank) => bank.id === gap.bankId), 'optionalAccess', _55 => _55.choices]);
294
1381
  }
295
1382
  function placeholderCounts(passage) {
296
1383
  const counts = /* @__PURE__ */ new Map();
@@ -492,10 +1579,238 @@ var RedactedWrittenResponseDataSchema = _v4.z.strictObject({
492
1579
  rubric: RedactedWrittenResponseRubricSchema.optional(),
493
1580
  languageTarget: _v4.z.string().optional()
494
1581
  });
1582
+ var RedactedDictationSlowMediaSchema = _v4.z.strictObject({
1583
+ type: _v4.z.literal("audio"),
1584
+ url: MediaUrlSchema,
1585
+ alt: _v4.z.string().min(1).optional()
1586
+ });
1587
+ var RedactedDictationDataSchema = _v4.z.strictObject({
1588
+ ...redactedBase,
1589
+ type: _v4.z.literal("dictation"),
1590
+ slowMedia: RedactedDictationSlowMediaSchema.optional(),
1591
+ hints: _v4.z.strictObject({ mode: _v4.z.literal("progressive-words") }).optional()
1592
+ }).check((ctx) => {
1593
+ const data = ctx.value;
1594
+ if (_optionalChain([data, 'access', _56 => _56.media, 'optionalAccess', _57 => _57.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 && _optionalChain([data, 'access', _58 => _58.media, 'optionalAccess', _59 => _59.playback, 'optionalAccess', _60 => _60.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
+ }
1618
+ });
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
+ }
495
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 _optionalChain([response, 'optionalAccess', _61 => _61.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;
@@ -1683,7 +3390,7 @@ function choicesFor(data, gap) {
1683
3390
  if (gap.choices !== void 0) {
1684
3391
  return gap.choices;
1685
3392
  }
1686
- return _nullishCoalesce(_optionalChain([data, 'access', _13 => _13.banks, 'optionalAccess', _14 => _14.find, 'call', _15 => _15((bank) => bank.id === gap.bankId), 'optionalAccess', _16 => _16.choices]), () => ( []));
3393
+ return _nullishCoalesce(_optionalChain([data, 'access', _62 => _62.banks, 'optionalAccess', _63 => _63.find, 'call', _64 => _64((bank) => bank.id === gap.bankId), 'optionalAccess', _65 => _65.choices]), () => ( []));
1687
3394
  }
1688
3395
  function scoreGapSelect(data, response) {
1689
3396
  const details = [];
@@ -1719,7 +3426,7 @@ function scoreMultipleChoice(data, response) {
1719
3426
  let scoreValue;
1720
3427
  if (data.scoringStrategy === "all-or-nothing") {
1721
3428
  if (data.mode === "single") {
1722
- scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _17 => _17.get, 'call', _18 => _18(response.selectedOptionIds[0]), 'optionalAccess', _19 => _19.isCorrect]) === true ? 1 : 0;
3429
+ scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _66 => _66.get, 'call', _67 => _67(response.selectedOptionIds[0]), 'optionalAccess', _68 => _68.isCorrect]) === true ? 1 : 0;
1723
3430
  } else {
1724
3431
  const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
1725
3432
  const allCorrectSelected = correctIds.every((id) => selected.has(id));
@@ -1730,7 +3437,7 @@ function scoreMultipleChoice(data, response) {
1730
3437
  let incorrectSelected = 0;
1731
3438
  for (const id of selected) {
1732
3439
  const option = optionById.get(id);
1733
- if (_optionalChain([option, 'optionalAccess', _20 => _20.isCorrect])) {
3440
+ if (_optionalChain([option, 'optionalAccess', _69 => _69.isCorrect])) {
1734
3441
  correctSelected += 1;
1735
3442
  } else {
1736
3443
  incorrectSelected += 1;
@@ -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.
@@ -1892,7 +3608,7 @@ var multipleChoiceType = defineActivityType({
1892
3608
  // exact optionals. Structurally identical at runtime — cast is type-level only.
1893
3609
  schema: MultipleChoiceDataSchema,
1894
3610
  scoring: { kind: "sync", score: scoreMultipleChoice },
1895
- isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _21 => _21.selectedOptionIds, 'access', _22 => _22.length]), () => ( 0))) > 0,
3611
+ isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _70 => _70.selectedOptionIds, 'access', _71 => _71.length]), () => ( 0))) > 0,
1896
3612
  fieldPolicy: MULTIPLE_CHOICE_FIELD_POLICY,
1897
3613
  redactedSchema: RedactedMultipleChoiceDataSchema,
1898
3614
  interop: {
@@ -1909,7 +3625,7 @@ var fillInTheBlanksType = defineActivityType({
1909
3625
  type: "fill-in-the-blanks",
1910
3626
  schema: FillInTheBlanksDataSchema,
1911
3627
  scoring: { kind: "sync", score: scoreFillInTheBlanks },
1912
- isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _23 => _23.answers]), () => ( {}))).some((answer) => answer.trim().length > 0),
3628
+ isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _72 => _72.answers]), () => ( {}))).some((answer) => answer.trim().length > 0),
1913
3629
  fieldPolicy: FILL_IN_THE_BLANKS_FIELD_POLICY,
1914
3630
  redactedSchema: RedactedFillInTheBlanksDataSchema,
1915
3631
  interop: {
@@ -1932,14 +3648,14 @@ var writtenResponseType = defineActivityType({
1932
3648
  kind: "deferred",
1933
3649
  reason: "requires_async_grading",
1934
3650
  partial: (data, response) => {
1935
- const wordCount = countWords(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _24 => _24.text]), () => ( "")));
3651
+ const wordCount = countWords(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _73 => _73.text]), () => ( "")));
1936
3652
  return {
1937
3653
  withinWordBounds: response !== void 0 && wordCount >= data.minWords && wordCount <= data.maxWords,
1938
3654
  wordCount
1939
3655
  };
1940
3656
  }
1941
3657
  },
1942
- isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _25 => _25.text, 'access', _26 => _26.trim, 'call', _27 => _27(), 'access', _28 => _28.length]), () => ( 0))) > 0,
3658
+ isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _74 => _74.text, 'access', _75 => _75.trim, 'call', _76 => _76(), 'access', _77 => _77.length]), () => ( 0))) > 0,
1943
3659
  fieldPolicy: WRITTEN_RESPONSE_FIELD_POLICY,
1944
3660
  redactedSchema: RedactedWrittenResponseDataSchema,
1945
3661
  interop: {
@@ -1954,7 +3670,7 @@ var gapSelectType = defineActivityType({
1954
3670
  type: "gap-select",
1955
3671
  schema: GapSelectDataSchema,
1956
3672
  scoring: { kind: "sync", score: scoreGapSelect },
1957
- isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _29 => _29.selections]), () => ( {}))).some((choiceId) => choiceId !== ""),
3673
+ isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _78 => _78.selections]), () => ( {}))).some((choiceId) => choiceId !== ""),
1958
3674
  fieldPolicy: GAP_SELECT_FIELD_POLICY,
1959
3675
  redactedSchema: RedactedGapSelectDataSchema,
1960
3676
  interop: {
@@ -1973,10 +3689,61 @@ 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 _optionalChain([response, 'optionalAccess', _79 => _79.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, ..._nullishCoalesce(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);
3725
+
3726
+
3727
+
3728
+
3729
+
3730
+
3731
+
3732
+
3733
+
3734
+
3735
+
3736
+
3737
+
3738
+
3739
+
3740
+
3741
+
3742
+
3743
+
3744
+
3745
+
3746
+
1980
3747
 
1981
3748
 
1982
3749
 
@@ -2032,5 +3799,5 @@ registerActivityType(gapSelectType);
2032
3799
 
2033
3800
 
2034
3801
 
2035
- exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.NativeControlHintSchema = NativeControlHintSchema; exports.MediaPlaybackSchema = MediaPlaybackSchema; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.isUnset = isUnset; exports.isUnwritten = isUnwritten; exports.isMissingId = isMissingId; exports.checkMedia = checkMedia; exports.covers = covers; exports.MultipleChoiceOptionMediaSchema = MultipleChoiceOptionMediaSchema; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.countWords = countWords; exports.GapSelectChoiceSchema = GapSelectChoiceSchema; exports.GapSelectBankSchema = GapSelectBankSchema; exports.GapSelectGapSchema = GapSelectGapSchema; exports.GapSelectDataSchema = GapSelectDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionMediaSchema = RedactedMultipleChoiceOptionMediaSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedGapSelectChoiceSchema = RedactedGapSelectChoiceSchema; exports.RedactedGapSelectBankSchema = RedactedGapSelectBankSchema; exports.RedactedGapSelectGapSchema = RedactedGapSelectGapSchema; exports.RedactedGapSelectDataSchema = RedactedGapSelectDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType;
2036
- //# sourceMappingURL=chunk-J6POE4HL.cjs.map
3802
+ exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.withValidationScope = withValidationScope; exports.DICTATION_MAX_TRANSCRIPT_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH; exports.DICTATION_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH; exports.DICTATION_MAX_EQUIVALENCE_LENGTH = DICTATION_MAX_EQUIVALENCE_LENGTH; exports.DICTATION_MAX_EQUIVALENCES = DICTATION_MAX_EQUIVALENCES; exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = DICTATION_MAX_ACCEPTED_TRANSCRIPTS; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.NativeControlHintSchema = NativeControlHintSchema; exports.MediaPlaybackSchema = MediaPlaybackSchema; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.DictationSlowMediaSchema = DictationSlowMediaSchema; exports.DictationEquivalenceSchema = DictationEquivalenceSchema; exports.DictationToleranceSchema = DictationToleranceSchema; exports.DictationDataSchema = DictationDataSchema; exports.groupCaptionsRevealDictation = groupCaptionsRevealDictation; exports.GROUP_CAPTIONS_REVEAL_DICTATION = GROUP_CAPTIONS_REVEAL_DICTATION; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.isUnset = isUnset; exports.isUnwritten = isUnwritten; exports.isMissingId = isMissingId; exports.checkMedia = checkMedia; exports.covers = covers; exports.pathKey = pathKey; exports.coveredPathsOf = coveredPathsOf; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.MultipleChoiceOptionMediaSchema = MultipleChoiceOptionMediaSchema; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.countWords = countWords; exports.GapSelectChoiceSchema = GapSelectChoiceSchema; exports.GapSelectBankSchema = GapSelectBankSchema; exports.GapSelectGapSchema = GapSelectGapSchema; exports.GapSelectDataSchema = GapSelectDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionMediaSchema = RedactedMultipleChoiceOptionMediaSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedGapSelectChoiceSchema = RedactedGapSelectChoiceSchema; exports.RedactedGapSelectBankSchema = RedactedGapSelectBankSchema; exports.RedactedGapSelectGapSchema = RedactedGapSelectGapSchema; exports.RedactedGapSelectDataSchema = RedactedGapSelectDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.RedactedDictationSlowMediaSchema = RedactedDictationSlowMediaSchema; exports.RedactedDictationDataSchema = RedactedDictationDataSchema; exports.dictationReferenceWords = dictationReferenceWords; exports.alignDictation = alignDictation; exports.STEP = STEP; exports.alignSequences = alignSequences; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType; exports.gapSelectType = gapSelectType; exports.dictationType = dictationType;
3803
+ //# sourceMappingURL=chunk-XONKWL6D.cjs.map