@intellectif/lk-core 1.1.0 → 1.3.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 (99) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/ai-B222dtHH.d.cts +2154 -0
  4. package/dist/ai-B222dtHH.d.cts.map +1 -0
  5. package/dist/ai-B222dtHH.d.ts +2154 -0
  6. package/dist/ai-B222dtHH.d.ts.map +1 -0
  7. package/dist/ai-check.cjs +758 -464
  8. package/dist/ai-check.cjs.map +1 -1
  9. package/dist/ai-check.d.cts +82 -68
  10. package/dist/ai-check.d.cts.map +1 -0
  11. package/dist/ai-check.d.ts +82 -68
  12. package/dist/ai-check.d.ts.map +1 -0
  13. package/dist/ai-check.js +756 -465
  14. package/dist/ai-check.js.map +1 -1
  15. package/dist/ai-writing-CCBjTOM3.js +2496 -0
  16. package/dist/ai-writing-CCBjTOM3.js.map +1 -0
  17. package/dist/ai-writing-DCVkv4Zj.cjs +2735 -0
  18. package/dist/ai-writing-DCVkv4Zj.cjs.map +1 -0
  19. package/dist/{index-DlcqkM_L.d.ts → index-BWPRPIXL.d.ts} +263 -258
  20. package/dist/index-BWPRPIXL.d.ts.map +1 -0
  21. package/dist/index-CXyOV8sm.d.cts +153 -0
  22. package/dist/index-CXyOV8sm.d.cts.map +1 -0
  23. package/dist/index-CegM8o15.d.ts +153 -0
  24. package/dist/index-CegM8o15.d.ts.map +1 -0
  25. package/dist/index-Dyu66R4-.d.cts +1071 -0
  26. package/dist/index-Dyu66R4-.d.cts.map +1 -0
  27. package/dist/{index-DlCW0XaQ.d.cts → index-O4_ExlAD.d.cts} +263 -258
  28. package/dist/index-O4_ExlAD.d.cts.map +1 -0
  29. package/dist/index-UGxRSopE.d.ts +1071 -0
  30. package/dist/index-UGxRSopE.d.ts.map +1 -0
  31. package/dist/index.cjs +1210 -1671
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +1090 -853
  34. package/dist/index.d.cts.map +1 -0
  35. package/dist/index.d.ts +1090 -853
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1063 -1672
  38. package/dist/index.js.map +1 -1
  39. package/dist/item-group-DXiR8nOr.js +445 -0
  40. package/dist/item-group-DXiR8nOr.js.map +1 -0
  41. package/dist/item-group-DroFXgLb.cjs +505 -0
  42. package/dist/item-group-DroFXgLb.cjs.map +1 -0
  43. package/dist/registry-DVi0LR_i.cjs +5915 -0
  44. package/dist/registry-DVi0LR_i.cjs.map +1 -0
  45. package/dist/registry-Dsx9mNsS.js +5449 -0
  46. package/dist/registry-Dsx9mNsS.js.map +1 -0
  47. package/dist/schemas-Bdw0nGPo.js +152 -0
  48. package/dist/schemas-Bdw0nGPo.js.map +1 -0
  49. package/dist/schemas-DVbAOrtd.cjs +224 -0
  50. package/dist/schemas-DVbAOrtd.cjs.map +1 -0
  51. package/dist/schemas.cjs +16 -32
  52. package/dist/schemas.d.cts +2 -2
  53. package/dist/schemas.d.ts +2 -2
  54. package/dist/schemas.js +3 -32
  55. package/dist/scoring-B2_HEhMT.cjs +1748 -0
  56. package/dist/scoring-B2_HEhMT.cjs.map +1 -0
  57. package/dist/scoring-BZqUkuDF.js +1604 -0
  58. package/dist/scoring-BZqUkuDF.js.map +1 -0
  59. package/dist/scoring.cjs +37 -75
  60. package/dist/scoring.d.cts +3 -1103
  61. package/dist/scoring.d.ts +3 -1103
  62. package/dist/scoring.js +3 -75
  63. package/dist/xapi-CMTph7wG.js +332 -0
  64. package/dist/xapi-CMTph7wG.js.map +1 -0
  65. package/dist/xapi-DNzkSJ0S.cjs +362 -0
  66. package/dist/xapi-DNzkSJ0S.cjs.map +1 -0
  67. package/dist/xapi.cjs +7 -16
  68. package/dist/xapi.d.cts +2 -150
  69. package/dist/xapi.d.ts +2 -150
  70. package/dist/xapi.js +2 -16
  71. package/package.json +4 -4
  72. package/dist/activity-MBOYLBjR.d.cts +0 -1840
  73. package/dist/activity-MBOYLBjR.d.ts +0 -1840
  74. package/dist/chunk-2JT2WUQ2.cjs +0 -458
  75. package/dist/chunk-2JT2WUQ2.cjs.map +0 -1
  76. package/dist/chunk-DSDXL3JO.cjs +0 -4597
  77. package/dist/chunk-DSDXL3JO.cjs.map +0 -1
  78. package/dist/chunk-FBKTION6.js +0 -4597
  79. package/dist/chunk-FBKTION6.js.map +0 -1
  80. package/dist/chunk-IARLJ5MW.js +0 -1256
  81. package/dist/chunk-IARLJ5MW.js.map +0 -1
  82. package/dist/chunk-LDFQUZII.cjs +0 -950
  83. package/dist/chunk-LDFQUZII.cjs.map +0 -1
  84. package/dist/chunk-NQHETFDA.js +0 -950
  85. package/dist/chunk-NQHETFDA.js.map +0 -1
  86. package/dist/chunk-SJVRUPJT.js +0 -272
  87. package/dist/chunk-SJVRUPJT.js.map +0 -1
  88. package/dist/chunk-TJJFKKH2.cjs +0 -272
  89. package/dist/chunk-TJJFKKH2.cjs.map +0 -1
  90. package/dist/chunk-VSH7ECQL.cjs +0 -1256
  91. package/dist/chunk-VSH7ECQL.cjs.map +0 -1
  92. package/dist/chunk-XS6U2ARZ.js +0 -458
  93. package/dist/chunk-XS6U2ARZ.js.map +0 -1
  94. package/dist/schemas.cjs.map +0 -1
  95. package/dist/schemas.js.map +0 -1
  96. package/dist/scoring.cjs.map +0 -1
  97. package/dist/scoring.js.map +0 -1
  98. package/dist/xapi.cjs.map +0 -1
  99. package/dist/xapi.js.map +0 -1
@@ -1,4597 +0,0 @@
1
- // src/schemas/media.ts
2
- import { z } from "zod/v4";
3
-
4
- // src/schemas/text-length.ts
5
- function maxUnits(schema, max) {
6
- return schema.check((payload) => {
7
- if (payload.value.length > max) {
8
- payload.issues.push({
9
- code: "too_big",
10
- origin: "string",
11
- maximum: max,
12
- inclusive: true,
13
- input: payload.value
14
- });
15
- }
16
- }).meta({ maxLength: max });
17
- }
18
-
19
- // src/schemas/media.ts
20
- var MediaUrlSchema = z.union([
21
- z.url().refine((value) => /^(https?|data|blob):/i.test(value), {
22
- error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
23
- }),
24
- z.string().regex(/^\/(?!\/)\S*$/, {
25
- error: 'Relative media URLs must be root-relative (a single leading "/").'
26
- })
27
- ]);
28
- var NativeControlHintSchema = z.enum(["hide-download", "hide-rate"]);
29
- var MediaPlaybackSchema = z.strictObject({
30
- /**
31
- * `native` (the resolved default) renders today's `<audio controls>`
32
- * unchanged. `minimal` — resolved automatically whenever any enforcement
33
- * field is set — replaces the browser bar with the SDK transport:
34
- * play/pause, elapsed/total, mute, volume, optional speed, optional
35
- * scrubber, and a live plays-remaining status.
36
- */
37
- controls: z.enum(["native", "minimal"]).optional(),
38
- /**
39
- * How many times the recording may be STARTED. A play is consumed when
40
- * playback begins from anywhere other than where it last stopped, so
41
- * pausing, resuming, and paging between the questions of one listening
42
- * group are all free. Enforced in `practice` and `exam`; never in `review`.
43
- */
44
- maxPlays: z.number().int().min(1).max(20).optional(),
45
- /** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
46
- seek: z.enum(["allow", "none"]).optional(),
47
- /** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
48
- rate: z.enum(["allow", "fixed"]).optional(),
49
- /** Advisory only. See {@link NativeControlHintSchema}. */
50
- nativeControlHints: z.array(NativeControlHintSchema).min(1).max(2).optional()
51
- });
52
- var MediaTrackSchema = z.strictObject({
53
- kind: z.enum(["captions", "subtitles"]),
54
- src: MediaUrlSchema,
55
- srclang: z.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
56
- error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
57
- }),
58
- label: maxUnits(z.string(), 60).refine((label) => label.trim().length > 0, {
59
- error: "A track label must not be empty."
60
- }),
61
- default: z.boolean().optional()
62
- });
63
- var MAX_TRACKS = 12;
64
- function posterFits(m) {
65
- return m.poster === void 0 || m.type === "video";
66
- }
67
- function tracksFit(m) {
68
- return m.tracks === void 0 || m.type === "audio" || m.type === "video";
69
- }
70
- function tracksAreDistinct(m) {
71
- if (m.tracks === void 0) {
72
- return true;
73
- }
74
- const defaults = m.tracks.filter((track) => track.default === true).length;
75
- const keys = new Set(m.tracks.map((track) => `${track.kind}:${track.srclang.toLowerCase()}`));
76
- return defaults <= 1 && keys.size === m.tracks.length;
77
- }
78
- var POSTER_FITS = { error: "poster is for video media only.", path: ["poster"] };
79
- var TRACKS_FIT = { error: "tracks are for audio and video media only.", path: ["tracks"] };
80
- var TRACKS_DISTINCT = {
81
- error: "tracks: at most one may be the default, and no two may share a kind and a language \u2014 a menu listing one language twice cannot say which one it plays.",
82
- path: ["tracks"]
83
- };
84
- var MediaSchema = z.looseObject({
85
- type: z.enum(["image", "audio", "video", "embed"]),
86
- url: MediaUrlSchema,
87
- alt: z.string().min(1).optional(),
88
- captionsUrl: MediaUrlSchema.optional(),
89
- tracks: z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
90
- poster: MediaUrlSchema.optional(),
91
- playback: MediaPlaybackSchema.optional()
92
- }).refine(
93
- (m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
94
- {
95
- error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
96
- path: ["alt"]
97
- }
98
- ).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
99
- error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
100
- path: ["url"]
101
- }).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT).refine((m) => m.playback === void 0 || m.type === "audio", {
102
- 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.",
103
- path: ["playback"]
104
- }).refine(
105
- (m) => m.playback?.controls !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
106
- {
107
- 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.',
108
- path: ["playback", "controls"]
109
- }
110
- ).refine((m) => m.playback?.maxPlays === void 0 || m.playback.seek !== "allow", {
111
- 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.',
112
- path: ["playback", "seek"]
113
- }).refine(
114
- (m) => m.playback?.controls !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
115
- {
116
- 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`.',
117
- path: ["playback", "controls"]
118
- }
119
- ).refine(
120
- (m) => m.playback?.nativeControlHints === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
121
- {
122
- 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.",
123
- path: ["playback", "nativeControlHints"]
124
- }
125
- );
126
- var RedactedMediaSchema = z.strictObject({
127
- type: z.enum(["image", "audio", "video", "embed"]),
128
- url: MediaUrlSchema,
129
- alt: z.string().min(1).optional(),
130
- captionsUrl: MediaUrlSchema.optional(),
131
- tracks: z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
132
- poster: MediaUrlSchema.optional(),
133
- playback: MediaPlaybackSchema.optional()
134
- }).refine(
135
- (m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
136
- {
137
- error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
138
- path: ["alt"]
139
- }
140
- ).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
141
- error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
142
- path: ["url"]
143
- }).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
144
-
145
- // src/validation-scope.ts
146
- var active = null;
147
- function withValidationScope(run) {
148
- if (active !== null) {
149
- return run();
150
- }
151
- active = /* @__PURE__ */ new Map();
152
- try {
153
- return run();
154
- } finally {
155
- active = null;
156
- }
157
- }
158
- function scopeCache(key, create) {
159
- if (active === null) {
160
- return void 0;
161
- }
162
- let value = active.get(key);
163
- if (value === void 0) {
164
- value = create();
165
- active.set(key, value);
166
- }
167
- return value;
168
- }
169
-
170
- // src/scoring/dictation/normalize.ts
171
- var DICTATION_MAX_TRANSCRIPT_LENGTH = 2e3;
172
- var DICTATION_MAX_TEXT_LENGTH = 8e3;
173
- var DICTATION_MAX_EQUIVALENCE_LENGTH = 200;
174
- var DICTATION_MAX_EQUIVALENCES = 100;
175
- var DICTATION_MAX_ACCEPTED_TRANSCRIPTS = 10;
176
- var WORKING_LENGTH = 2 * DICTATION_MAX_TEXT_LENGTH;
177
- var UNSPACED_REVEAL_MIN_CHARACTERS = 4;
178
- var cp = (...points) => String.fromCodePoint(...points);
179
- var NEXT_LINE = cp(133);
180
- var MIDDLE_DOT = cp(183);
181
- var TIBETAN_TSHEG = cp(3851);
182
- var COMBINING_KEYCAP = cp(8419);
183
- var WAVING_BLACK_FLAG = 127988;
184
- var ZWNJ = 8204;
185
- var ZWJ = 8205;
186
- var MONGOLIAN_VOWEL_SEPARATOR = 6158;
187
- var JOINERS = cp(ZWNJ, ZWJ, MONGOLIAN_VOWEL_SEPARATOR);
188
- var MONGOLIAN_SELECTORS = cp(6155, 6156, 6157, 6159);
189
- var TAGS = `${cp(917536)}-${cp(917631)}`;
190
- var STRETCHERS = cp(1600, 2042);
191
- var COMPATIBILITY_RE = new RegExp(
192
- `[${[
193
- cp(170),
194
- cp(178, 179, 181, 185, 186),
195
- `${cp(188)}-${cp(190)}`,
196
- `${cp(306)}-${cp(307)}`,
197
- `${cp(319)}-${cp(320)}`,
198
- cp(329),
199
- `${cp(452)}-${cp(460)}`,
200
- `${cp(497)}-${cp(499)}`,
201
- `${cp(8304)}-${cp(8351)}`,
202
- `${cp(8448)}-${cp(8591)}`,
203
- `${cp(9312)}-${cp(9471)}`,
204
- `${cp(11904)}-${cp(12255)}`,
205
- `${cp(12881)}-${cp(12895)}`,
206
- `${cp(12977)}-${cp(12991)}`,
207
- `${cp(64256)}-${cp(65023)}`,
208
- `${cp(65040)}-${cp(65049)}`,
209
- `${cp(65072)}-${cp(65135)}`,
210
- `${cp(65136)}-${cp(65278)}`,
211
- `${cp(65281)}-${cp(65373)}`,
212
- `${cp(65375)}-${cp(65506)}`,
213
- `${cp(65508)}-${cp(65518)}`
214
- ].join("")}]`,
215
- "gu"
216
- );
217
- var COMPATIBILITY_JAMO = new Map(
218
- Array.from({ length: 12686 - 12593 + 1 }, (_, index) => {
219
- const jamo = cp(12593 + index);
220
- return [jamo.normalize("NFKC"), jamo];
221
- })
222
- );
223
- var SEQUENCE_FOLDS = /* @__PURE__ */ new Map([
224
- [cp(3363, 3405, ZWJ), cp(3450)],
225
- [cp(3368, 3405, ZWJ), cp(3451)],
226
- [cp(3376, 3405, ZWJ), cp(3452)],
227
- [cp(3378, 3405, ZWJ), cp(3453)],
228
- [cp(3379, 3405, ZWJ), cp(3454)],
229
- [cp(3349, 3405, ZWJ), cp(3455)],
230
- [cp(3374, 3405, ZWJ), cp(3412)],
231
- [cp(3375, 3405, ZWJ), cp(3413)],
232
- [cp(3380, 3405, ZWJ), cp(3414)],
233
- [cp(2468, 2509, ZWJ), cp(2510)],
234
- [cp(3368, 3405, 3377), cp(3451, 3405, 3377)],
235
- [cp(2352, 2381, ZWJ), cp(2353, 2381)],
236
- [cp(2353, 2381, ZWJ), cp(2353, 2381)]
237
- ]);
238
- var SEQUENCE_FOLDS_RE = new RegExp([...SEQUENCE_FOLDS.keys()].join("|"), "gu");
239
- var THAI_SARA_AM_PARTS_RE = new RegExp(
240
- `${cp(3661)}([${cp(3656)}-${cp(3659)}]*)${cp(3634)}`,
241
- "gu"
242
- );
243
- var LAO_SARA_AM_PARTS_RE = new RegExp(
244
- `${cp(3789)}([${cp(3784)}-${cp(3787)}]*)${cp(3762)}`,
245
- "gu"
246
- );
247
- function foldSequences(text) {
248
- 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)}`);
249
- }
250
- var FOLDED = new Map(
251
- [
252
- [8216, "'"],
253
- // LEFT SINGLE QUOTATION MARK
254
- [8217, "'"],
255
- // RIGHT SINGLE QUOTATION MARK (the smart apostrophe)
256
- [8218, "'"],
257
- // SINGLE LOW-9 QUOTATION MARK
258
- [8219, "'"],
259
- // SINGLE HIGH-REVERSED-9 QUOTATION MARK
260
- [8242, "'"],
261
- // PRIME
262
- [700, "'"],
263
- // MODIFIER LETTER APOSTROPHE
264
- [180, "'"],
265
- // ACUTE ACCENT, used as an apostrophe
266
- [96, "'"],
267
- // GRAVE ACCENT, used as an apostrophe
268
- [1523, "'"],
269
- // HEBREW PUNCTUATION GERESH
270
- [8220, '"'],
271
- // LEFT DOUBLE QUOTATION MARK
272
- [8221, '"'],
273
- // RIGHT DOUBLE QUOTATION MARK
274
- [8222, '"'],
275
- // DOUBLE LOW-9 QUOTATION MARK
276
- [8243, '"'],
277
- // DOUBLE PRIME
278
- [1524, '"'],
279
- // HEBREW PUNCTUATION GERSHAYIM
280
- [8208, "-"],
281
- // HYPHEN
282
- [8209, "-"],
283
- // NON-BREAKING HYPHEN
284
- [1470, "-"],
285
- // HEBREW PUNCTUATION MAQAF
286
- [1418, "-"],
287
- // ARMENIAN HYPHEN
288
- [3852, TIBETAN_TSHEG],
289
- // TIBETAN MARK DELIMITER TSHEG BSTAR
290
- [65374, cp(12316)]
291
- // FULLWIDTH TILDE, which a Japanese keyboard types for the WAVE DASH
292
- ].map(([point, folded]) => [cp(point), folded])
293
- );
294
- var FOLDED_RE = new RegExp(`[${[...FOLDED.keys()].join("")}]`, "gu");
295
- var DRAWN_FORMAT_CHARACTERS = `${cp(1536)}-${cp(1541)}${cp(1757, 1807, 2192, 2193, 2274, 69821, 69837)}`;
296
- var INVISIBLE_RE = new RegExp(
297
- `(?![\\t\\n\\v\\f\\r${NEXT_LINE}${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}${DRAWN_FORMAT_CHARACTERS}])[\\p{Cc}\\p{Cf}\\p{Default_Ignorable_Code_Point}${STRETCHERS}]`,
298
- "gu"
299
- );
300
- var REPEATED_JOINER_RE = new RegExp(`([${JOINERS}])\\1+`, "gu");
301
- var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
302
- var REPLACEMENT_CHARACTER = cp(65533);
303
- var CONTEXTUAL_RE = new RegExp(`[${JOINERS}${MONGOLIAN_SELECTORS}${TAGS}]`, "u");
304
- var WHITESPACE_RE = new RegExp(`[\\s${NEXT_LINE}]+`, "gu");
305
- 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}";
306
- var UNSPACED_CHARACTER = `(?!\\p{Nd})[${UNSPACED_CLASS}]`;
307
- var IN_WORD_PUNCTUATION = `'\\-${MIDDLE_DOT}${TIBETAN_TSHEG}`;
308
- var PUNCTUATION_RE = new RegExp(
309
- [
310
- `[${IN_WORD_PUNCTUATION}](?<![\\p{L}\\p{M}\\p{N}][${IN_WORD_PUNCTUATION}])`,
311
- `[${IN_WORD_PUNCTUATION}](?![\\p{L}\\p{M}\\p{N}])`,
312
- `${MIDDLE_DOT}(?<=${UNSPACED_CHARACTER}${MIDDLE_DOT})`,
313
- `${MIDDLE_DOT}(?=${UNSPACED_CHARACTER})`,
314
- `(?![#*]${COMBINING_KEYCAP})[^\\P{P}${IN_WORD_PUNCTUATION}]`
315
- ].join("|"),
316
- "gu"
317
- );
318
- var ALL_PUNCTUATION_RE = new RegExp(`(?![#*]${COMBINING_KEYCAP})\\p{P}`, "gu");
319
- var DIGIT_SEPARATOR_RE = new RegExp(`(?<=\\p{N})[.,'${cp(1643, 1644)}](?=\\p{N})`, "gu");
320
- var UNSPACED_LETTERS_RE = new RegExp(`(?![\\p{M}\\p{Nd}])[${UNSPACED_CLASS}]`, "gu");
321
- var SPACE_BESIDE_UNSPACED_RE = new RegExp(
322
- `(?<=${UNSPACED_CHARACTER}) (?=[\\p{L}\\p{M}\\p{N}])|(?<=[\\p{L}\\p{M}\\p{N}]) (?=${UNSPACED_CHARACTER})`,
323
- "gu"
324
- );
325
- var WORD_RE = new RegExp(`^[\\p{L}\\p{M}\\p{N}${JOINERS}]$`, "u");
326
- var UNSPACED_CHARACTER_RE = new RegExp(`^${UNSPACED_CHARACTER}$`, "u");
327
- var UNSPACED_LETTER_RE = new RegExp(`(?=\\p{L})${UNSPACED_CHARACTER}`, "u");
328
- function containsUnspacedScript(text) {
329
- return UNSPACED_LETTER_RE.test(text);
330
- }
331
- var EXTENDING_RE = new RegExp(`^[\\p{M}${JOINERS}${TAGS}]$`, "u");
332
- var WORD = 1;
333
- var UNSPACED = 2;
334
- var EXTENDING = 4;
335
- var KNOWN = 8;
336
- var FLAGS = [];
337
- function flagsOfCharacter(character) {
338
- return KNOWN | (WORD_RE.test(character) ? WORD : 0) | (UNSPACED_CHARACTER_RE.test(character) ? UNSPACED : 0) | (EXTENDING_RE.test(character) ? EXTENDING : 0);
339
- }
340
- function flagsAt(text, index) {
341
- const point = text.codePointAt(index);
342
- let plane = FLAGS[point >> 16];
343
- if (plane === void 0) {
344
- plane = new Uint8Array(65536);
345
- FLAGS[point >> 16] = plane;
346
- }
347
- const offset = point & 65535;
348
- let flags = plane[offset];
349
- if (flags === 0) {
350
- flags = flagsOfCharacter(String.fromCodePoint(point));
351
- plane[offset] = flags;
352
- }
353
- return flags;
354
- }
355
- var VIRAMAS = /* @__PURE__ */ new Set([
356
- 2381,
357
- 2509,
358
- 2637,
359
- 2765,
360
- 2893,
361
- 3021,
362
- 3149,
363
- 3277,
364
- 3387,
365
- 3388,
366
- 3405,
367
- 3530,
368
- 3642,
369
- 3770,
370
- 3972,
371
- 4153,
372
- 4154,
373
- 5908,
374
- 5909,
375
- 5940,
376
- 6098,
377
- 6752,
378
- 6980,
379
- 7082,
380
- 7083,
381
- 7154,
382
- 7155,
383
- 11647,
384
- 43014,
385
- 43052,
386
- 43204,
387
- 43347,
388
- 43456,
389
- 43766,
390
- 44013,
391
- 68159,
392
- 69702,
393
- 69744,
394
- 69759,
395
- 69817,
396
- 69939,
397
- 69940,
398
- 70080,
399
- 70197,
400
- 70378,
401
- 70477,
402
- 70606,
403
- 70607,
404
- 70608,
405
- 70722,
406
- 70850,
407
- 71103,
408
- 71231,
409
- 71350,
410
- 71467,
411
- 71737,
412
- 71997,
413
- 71998,
414
- 72160,
415
- 72244,
416
- 72263,
417
- 72345,
418
- 72767,
419
- 73028,
420
- 73029,
421
- 73111,
422
- 73537,
423
- 73538,
424
- 90415
425
- ]);
426
- var JOINS_FORWARD = [
427
- [1568, 1568],
428
- [1574, 1574],
429
- [1576, 1576],
430
- [1578, 1582],
431
- [1587, 1599],
432
- [1601, 1607],
433
- [1609, 1610],
434
- [1646, 1647],
435
- [1656, 1671],
436
- [1690, 1727],
437
- [1729, 1730],
438
- [1740, 1740],
439
- [1742, 1742],
440
- [1744, 1745],
441
- [1786, 1788],
442
- [1791, 1791],
443
- [1810, 1812],
444
- [1818, 1821],
445
- [1823, 1831],
446
- [1833, 1833],
447
- [1835, 1835],
448
- [1837, 1838],
449
- [1870, 1880],
450
- [1884, 1898],
451
- [1901, 1904],
452
- [1906, 1906],
453
- [1909, 1911],
454
- [1914, 1919],
455
- [1994, 2026],
456
- [2113, 2117],
457
- [2120, 2120],
458
- [2122, 2131],
459
- [2133, 2133],
460
- [2144, 2144],
461
- [2146, 2149],
462
- [2152, 2152],
463
- [2182, 2182],
464
- [2185, 2189],
465
- [2208, 2217],
466
- [2223, 2224],
467
- [2227, 2232],
468
- [2234, 2248],
469
- [6151, 6151],
470
- [6176, 6264],
471
- [6279, 6312],
472
- [6314, 6314],
473
- [43072, 43122],
474
- [68288, 68292],
475
- [68301, 68301],
476
- [68307, 68316],
477
- [68318, 68320],
478
- [68331, 68334],
479
- [68480, 68480],
480
- [68482, 68482],
481
- [68486, 68488],
482
- [68490, 68491],
483
- [68493, 68493],
484
- [68496, 68496],
485
- [68525, 68526],
486
- [68864, 68897],
487
- [68899, 68899],
488
- [69315, 69316],
489
- [69424, 69426],
490
- [69428, 69444],
491
- [69457, 69459],
492
- [69488, 69491],
493
- [69494, 69505],
494
- [69552, 69552],
495
- [69554, 69555],
496
- [69560, 69560],
497
- [69563, 69564],
498
- [69566, 69567],
499
- [69569, 69569],
500
- [69572, 69572],
501
- [69578, 69579],
502
- [125184, 125251]
503
- ];
504
- var JOINS_BACKWARD = [
505
- [1568, 1568],
506
- [1570, 1599],
507
- [1601, 1610],
508
- [1646, 1647],
509
- [1649, 1651],
510
- [1653, 1747],
511
- [1749, 1749],
512
- [1774, 1775],
513
- [1786, 1788],
514
- [1791, 1791],
515
- [1808, 1808],
516
- [1810, 1839],
517
- [1869, 1919],
518
- [1994, 2026],
519
- [2112, 2136],
520
- [2144, 2144],
521
- [2146, 2149],
522
- [2151, 2154],
523
- [2160, 2178],
524
- [2182, 2182],
525
- [2185, 2190],
526
- [2208, 2220],
527
- [2222, 2248],
528
- [6151, 6151],
529
- [6176, 6264],
530
- [6279, 6312],
531
- [6314, 6314],
532
- [43072, 43121],
533
- [68288, 68293],
534
- [68295, 68295],
535
- [68297, 68298],
536
- [68302, 68310],
537
- [68312, 68321],
538
- [68324, 68324],
539
- [68331, 68335],
540
- [68480, 68497],
541
- [68521, 68526],
542
- [68865, 68899],
543
- [69314, 69316],
544
- [69424, 69444],
545
- [69457, 69460],
546
- [69488, 69505],
547
- [69552, 69552],
548
- [69554, 69558],
549
- [69560, 69567],
550
- [69569, 69572],
551
- [69577, 69578],
552
- [125184, 125251]
553
- ];
554
- var TRANSPARENT_RE = new RegExp(`^[\\p{Mn}\\p{Me}${cp(125259)}${TAGS}]$`, "u");
555
- function collapseWhitespace(text) {
556
- return text.replace(WHITESPACE_RE, " ").trim();
557
- }
558
- function codePointLength(text) {
559
- let length = 0;
560
- for (const _ of text) {
561
- length += 1;
562
- }
563
- return length;
564
- }
565
- function truncateCodePoints(text, max) {
566
- if (text.length <= max) {
567
- return text;
568
- }
569
- let points = 0;
570
- let units = 0;
571
- for (const character of text) {
572
- if (points === max) {
573
- return text.slice(0, units);
574
- }
575
- points += 1;
576
- units += character.length;
577
- }
578
- return text;
579
- }
580
- function isLongerThan(text, max) {
581
- if (text.length <= max) {
582
- return false;
583
- }
584
- return text.length > 2 * max || truncateCodePoints(text, max).length < text.length;
585
- }
586
- function inRanges(point, ranges) {
587
- return ranges.some(([first, last]) => point >= first && point <= last);
588
- }
589
- function isMongolianLetterOrMark(point) {
590
- return point !== void 0 && point >= 6176 && point <= 6314;
591
- }
592
- function isMongolianSelector(point) {
593
- return point !== void 0 && (point >= 6155 && point <= 6157 || point === 6159);
594
- }
595
- function letterBefore(points, index) {
596
- for (let at = index - 1; at >= 0; at -= 1) {
597
- const point = points[at];
598
- if (!TRANSPARENT_RE.test(point)) {
599
- return point.codePointAt(0);
600
- }
601
- }
602
- return void 0;
603
- }
604
- function letterAfter(points, index) {
605
- for (let at = index + 1; at < points.length; at += 1) {
606
- const point = points[at];
607
- if (!TRANSPARENT_RE.test(point)) {
608
- return point.codePointAt(0);
609
- }
610
- }
611
- return void 0;
612
- }
613
- var LETTER_OR_MARK_RE = /^[\p{L}\p{M}]$/u;
614
- function keepsInContext(points, index) {
615
- const point = points[index].codePointAt(0);
616
- const previous = points[index - 1];
617
- const afterVirama = previous !== void 0 && VIRAMAS.has(previous.codePointAt(0));
618
- if (point === ZWJ) {
619
- const next = points[index + 1];
620
- return afterVirama || previous !== void 0 && LETTER_OR_MARK_RE.test(previous) && next !== void 0 && VIRAMAS.has(next.codePointAt(0));
621
- }
622
- if (point === ZWNJ) {
623
- if (afterVirama) {
624
- return true;
625
- }
626
- const before = letterBefore(points, index);
627
- const after = letterAfter(points, index);
628
- return before !== void 0 && after !== void 0 && inRanges(before, JOINS_FORWARD) && inRanges(after, JOINS_BACKWARD);
629
- }
630
- const previousPoint = previous?.codePointAt(0);
631
- if (point === MONGOLIAN_VOWEL_SEPARATOR) {
632
- return (isMongolianLetterOrMark(previousPoint) || isMongolianSelector(previousPoint)) && isMongolianLetterOrMark(points[index + 1]?.codePointAt(0));
633
- }
634
- return isMongolianLetterOrMark(previousPoint);
635
- }
636
- function settle(text) {
637
- let current = text;
638
- for (; ; ) {
639
- let next = foldSequences(current);
640
- if (CONTEXTUAL_RE.test(next)) {
641
- const points = Array.from(next.replace(REPEATED_JOINER_RE, "$1"));
642
- let inFlag = false;
643
- next = points.filter((character, index) => {
644
- const point = character.codePointAt(0);
645
- if (point >= 917536 && point <= 917631) {
646
- return inFlag;
647
- }
648
- inFlag = point === WAVING_BLACK_FLAG;
649
- return !CONTEXTUAL_RE.test(character) || keepsInContext(points, index);
650
- }).join("");
651
- }
652
- next = collapseWhitespace(next).normalize("NFC");
653
- if (next === current) {
654
- return current;
655
- }
656
- current = next;
657
- }
658
- }
659
- var CARRIED_MARKS_RE = new RegExp(`^[ ${cp(1600)}]?(\\p{M}+)$`, "u");
660
- var SPACING_CHARACTER_RE = new RegExp(`[\\s${NEXT_LINE}]`, "u");
661
- var PUNCTUATION_CHARACTER_RE = /^\p{P}$/u;
662
- function foldCompatibility(character, offset, text) {
663
- const folded = character.normalize("NFKC");
664
- const carried = CARRIED_MARKS_RE.exec(folded);
665
- if (carried === null) {
666
- return COMPATIBILITY_JAMO.get(folded) ?? folded;
667
- }
668
- return PUNCTUATION_CHARACTER_RE.test(character) || offset === 0 || SPACING_CHARACTER_RE.test(text.charAt(offset - 1)) ? character : carried[1];
669
- }
670
- function preStripNormalize(text) {
671
- return settle(
672
- collapseWhitespace(
673
- text.replace(LONE_SURROGATE_RE, REPLACEMENT_CHARACTER).normalize("NFC").replace(COMPATIBILITY_RE, foldCompatibility).replace(INVISIBLE_RE, "").replace(FOLDED_RE, (character) => FOLDED.get(character)).toLowerCase()
674
- ).normalize("NFC")
675
- );
676
- }
677
- function settleWithout(text, pattern, replacement) {
678
- let current = text;
679
- for (; ; ) {
680
- const next = settle(current.replace(pattern, replacement));
681
- if (next === current) {
682
- return current;
683
- }
684
- current = next;
685
- }
686
- }
687
- function stripPunctuation(text) {
688
- return settleWithout(text, PUNCTUATION_RE, "");
689
- }
690
- function spacePunctuation(text) {
691
- return settleWithout(text.replace(DIGIT_SEPARATOR_RE, ""), ALL_PUNCTUATION_RE, " ");
692
- }
693
- function isHighSurrogate(unit) {
694
- return unit >= 55296 && unit <= 56319;
695
- }
696
- function isLowSurrogate(unit) {
697
- return unit >= 56320 && unit <= 57343;
698
- }
699
- function startOfCodePointBefore(text, index) {
700
- return index >= 2 && isLowSurrogate(text.charCodeAt(index - 1)) && isHighSurrogate(text.charCodeAt(index - 2)) ? index - 2 : index - 1;
701
- }
702
- function lastCodePointAfter(previous, appended) {
703
- return appended === "" ? previous : appended.slice(startOfCodePointBefore(appended, appended.length));
704
- }
705
- function isAsciiWordUnit(unit) {
706
- return unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122;
707
- }
708
- function isWordAt(text, index) {
709
- const unit = text.charCodeAt(index);
710
- return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & WORD) !== 0;
711
- }
712
- function isSpacedWordAt(text, index) {
713
- const unit = text.charCodeAt(index);
714
- return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & (WORD | UNSPACED)) === WORD;
715
- }
716
- function isExtendingAt(text, index) {
717
- return text.charCodeAt(index) >= 128 && (flagsAt(text, index) & EXTENDING) !== 0;
718
- }
719
- function edgeOf(text, index, unspaced) {
720
- if (!isWordAt(text, index)) {
721
- return "none";
722
- }
723
- return isSpacedWordAt(text, index) ? "spaced" : unspaced;
724
- }
725
- function phraseOf(text, unspaced) {
726
- return {
727
- text,
728
- before: edgeOf(text, 0, unspaced),
729
- after: edgeOf(text, startOfCodePointBefore(text, text.length), unspaced)
730
- };
731
- }
732
- function blocks(edge, text, neighbour) {
733
- if (edge === "none") {
734
- return false;
735
- }
736
- return edge === "spaced" ? isSpacedWordAt(text, neighbour) : isWordAt(text, neighbour);
737
- }
738
- function isMatchAt(text, phrase, at) {
739
- const end = at + phrase.text.length;
740
- if (end < text.length && (isExtendingAt(text, end) || blocks(phrase.after, text, end))) {
741
- return false;
742
- }
743
- return !(at > 0 && blocks(phrase.before, text, startOfCodePointBefore(text, at)));
744
- }
745
- function indexOfPhrase(text, phrase, start) {
746
- const needle = phrase.text;
747
- let at = text.indexOf(needle, start);
748
- while (at !== -1) {
749
- if (phrase.before === "spaced" && at > 0 && isSpacedWordAt(text, startOfCodePointBefore(text, at))) {
750
- let next = at;
751
- while (next < text.length && isSpacedWordAt(text, next)) {
752
- next += isHighSurrogate(text.charCodeAt(next)) && isLowSurrogate(text.charCodeAt(next + 1)) ? 2 : 1;
753
- }
754
- at = text.indexOf(needle, next);
755
- continue;
756
- }
757
- if (isMatchAt(text, phrase, at)) {
758
- return at;
759
- }
760
- at = text.indexOf(needle, at + 1);
761
- }
762
- return -1;
763
- }
764
- function containsPhrase(text, phrase) {
765
- const needle = phrase.text;
766
- const length = needle.length;
767
- if (length === 0 || length > text.length) {
768
- return false;
769
- }
770
- const fallback = new Int32Array(length);
771
- for (let index = 1, matched = 0; index < length; index += 1) {
772
- while (matched > 0 && needle.charCodeAt(index) !== needle.charCodeAt(matched)) {
773
- matched = fallback[matched - 1];
774
- }
775
- if (needle.charCodeAt(index) === needle.charCodeAt(matched)) {
776
- matched += 1;
777
- }
778
- fallback[index] = matched;
779
- }
780
- for (let index = 0, matched = 0; index < text.length; index += 1) {
781
- while (matched > 0 && text.charCodeAt(index) !== needle.charCodeAt(matched)) {
782
- matched = fallback[matched - 1];
783
- }
784
- if (text.charCodeAt(index) === needle.charCodeAt(matched)) {
785
- matched += 1;
786
- }
787
- if (matched === length) {
788
- if (isMatchAt(text, phrase, index - length + 1)) {
789
- return true;
790
- }
791
- matched = fallback[matched - 1];
792
- }
793
- }
794
- return false;
795
- }
796
- function compileRules(tolerance) {
797
- const equivalences = tolerance?.equivalences;
798
- if (!Array.isArray(equivalences)) {
799
- return { rules: [], signature: "[]" };
800
- }
801
- const rules = [];
802
- for (const rule of equivalences.slice(0, DICTATION_MAX_EQUIVALENCES)) {
803
- const rawFrom = rule?.from;
804
- const rawTo = rule?.to;
805
- if (typeof rawFrom !== "string" || typeof rawTo !== "string" || isLongerThan(rawFrom, DICTATION_MAX_EQUIVALENCE_LENGTH) || isLongerThan(rawTo, DICTATION_MAX_EQUIVALENCE_LENGTH)) {
806
- continue;
807
- }
808
- const from = preStripNormalize(rawFrom);
809
- const to = stripPunctuation(preStripNormalize(rawTo));
810
- if (from === "" || to === "") {
811
- continue;
812
- }
813
- rules.push({
814
- from: phraseOf(from, "none"),
815
- to,
816
- spaceBefore: isSpacedWordAt(to, 0),
817
- spaceAfter: isSpacedWordAt(to, startOfCodePointBefore(to, to.length))
818
- });
819
- }
820
- return {
821
- rules,
822
- signature: JSON.stringify(rules.map((rule) => [rule.from.text, rule.to]))
823
- };
824
- }
825
- function bounded(text) {
826
- return isLongerThan(text, WORKING_LENGTH) ? { text: truncateCodePoints(text, WORKING_LENGTH), cut: true } : { text, cut: false };
827
- }
828
- function rewrite(text, rule) {
829
- let output = "";
830
- let consumed = 0;
831
- let last = "";
832
- for (let at = indexOfPhrase(text, rule.from, 0); at !== -1; at = indexOfPhrase(text, rule.from, consumed)) {
833
- const between = text.slice(consumed, at);
834
- if (rule.spaceAfter && consumed > 0 && between !== "" && isSpacedWordAt(between, 0)) {
835
- output += " ";
836
- }
837
- output += between;
838
- last = lastCodePointAfter(last, between);
839
- if (rule.spaceBefore && last !== "" && isSpacedWordAt(last, 0)) {
840
- output += " ";
841
- last = " ";
842
- }
843
- output += rule.to;
844
- last = lastCodePointAfter(last, rule.to);
845
- consumed = at + rule.from.text.length;
846
- if (output.length > 2 * WORKING_LENGTH) {
847
- return { text: truncateCodePoints(output, WORKING_LENGTH), cut: true };
848
- }
849
- }
850
- const rest = text.slice(consumed);
851
- const apart = rule.spaceAfter && consumed > 0 && rest !== "" && isSpacedWordAt(rest, 0);
852
- return bounded(apart ? `${output} ${rest}` : output + rest);
853
- }
854
- var WORKING_TEXTS = /* @__PURE__ */ Symbol("dictation working texts");
855
- var MEASURED_TRANSCRIPTS = /* @__PURE__ */ Symbol("dictation measured transcripts");
856
- var SHOWN_TEXTS = /* @__PURE__ */ Symbol("dictation shown texts");
857
- var REVEALS = /* @__PURE__ */ Symbol("dictation reveal answers");
858
- function scopedMap(key, signature) {
859
- const bySignature = scopeCache(key, () => /* @__PURE__ */ new Map());
860
- if (bySignature === void 0) {
861
- return /* @__PURE__ */ new Map();
862
- }
863
- let map = bySignature.get(signature);
864
- if (map === void 0) {
865
- map = /* @__PURE__ */ new Map();
866
- bySignature.set(signature, map);
867
- }
868
- return map;
869
- }
870
- var PRE_STRIP_TEXTS = /* @__PURE__ */ Symbol("dictation pre-strip texts");
871
- function boundedPreStrip(text) {
872
- const memo = scopeCache(PRE_STRIP_TEXTS, () => /* @__PURE__ */ new Map());
873
- let result = memo?.get(text);
874
- if (result === void 0) {
875
- result = bounded(preStripNormalize(text));
876
- memo?.set(text, result);
877
- }
878
- return result;
879
- }
880
- function isBlankBeforeRules(text) {
881
- return boundedPreStrip(text).text === "";
882
- }
883
- function workingText(text, compiled, preStripped = boundedPreStrip(text)) {
884
- if (compiled.rules.length === 0) {
885
- return preStripped;
886
- }
887
- const memo = scopeCache(WORKING_TEXTS, () => /* @__PURE__ */ new Map());
888
- let known = memo?.get(compiled.signature);
889
- const remembered = known?.get(text);
890
- if (remembered !== void 0) {
891
- return remembered;
892
- }
893
- let working = preStripped;
894
- let cut = working.cut;
895
- for (const rule of compiled.rules) {
896
- working = rewrite(working.text, rule);
897
- cut ||= working.cut;
898
- }
899
- const result = { text: working.text, cut };
900
- if (memo !== void 0) {
901
- if (known === void 0) {
902
- known = /* @__PURE__ */ new Map();
903
- memo.set(compiled.signature, known);
904
- }
905
- known.set(text, result);
906
- }
907
- return result;
908
- }
909
- function normalizeDictationTextBounded(text, tolerance) {
910
- const working = workingText(text, compileRules(tolerance));
911
- return { text: stripPunctuation(working.text), cut: working.cut };
912
- }
913
- function normalizeDictationText(text, tolerance) {
914
- return normalizeDictationTextBounded(text, tolerance).text;
915
- }
916
- var NOTHING_TO_REVEAL = { forms: [], long: false, before: "none", after: "none" };
917
- function stripForReveal(text) {
918
- return stripPunctuation(text.replace(DIGIT_SEPARATOR_RE, ""));
919
- }
920
- function readingOf(written, working) {
921
- const writtenStripped = stripForReveal(written);
922
- const workingStripped = stripForReveal(working);
923
- const source = writtenStripped === "" ? workingStripped : writtenStripped;
924
- if (source === "") {
925
- return NOTHING_TO_REVEAL;
926
- }
927
- const long = (source.match(UNSPACED_LETTERS_RE)?.length ?? 0) >= UNSPACED_REVEAL_MIN_CHARACTERS;
928
- const { before, after } = phraseOf(source, long ? "none" : "visible");
929
- const forms = [
930
- writtenStripped,
931
- spacePunctuation(written),
932
- workingStripped,
933
- spacePunctuation(working)
934
- ].map((form) => long ? form.replace(SPACE_BESIDE_UNSPACED_RE, "") : form);
935
- return { forms: [...new Set(forms)].filter((form) => form !== ""), long, before, after };
936
- }
937
- function dictationNormalizer(tolerance) {
938
- const compiled = compileRules(tolerance);
939
- const measured = scopedMap(MEASURED_TRANSCRIPTS, compiled.signature);
940
- const shownTexts = scopedMap(SHOWN_TEXTS, compiled.signature);
941
- return {
942
- measure(text) {
943
- let result = measured.get(text);
944
- if (result === void 0) {
945
- if (isLongerThan(text, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
946
- result = { tooLong: true, normalized: null, reveal: NOTHING_TO_REVEAL };
947
- } else {
948
- const written = boundedPreStrip(text);
949
- const working = workingText(text, compiled, written);
950
- const normalized = stripPunctuation(working.text);
951
- result = {
952
- tooLong: working.cut || isLongerThan(normalized, DICTATION_MAX_TRANSCRIPT_LENGTH),
953
- normalized,
954
- reveal: readingOf(written.text, working.text)
955
- };
956
- }
957
- measured.set(text, result);
958
- }
959
- return result;
960
- },
961
- shown(text) {
962
- let result = shownTexts.get(text);
963
- if (result === void 0) {
964
- const plainText = isLongerThan(text, WORKING_LENGTH) ? null : boundedPreStrip(text);
965
- const working = plainText === null ? null : workingText(text, compiled, plainText);
966
- if (plainText === null || working === null || working.cut) {
967
- result = { tooLong: true, forms: [] };
968
- } else {
969
- const plain = plainText.text;
970
- result = {
971
- tooLong: false,
972
- forms: [
973
- .../* @__PURE__ */ new Set([
974
- stripForReveal(working.text),
975
- spacePunctuation(working.text),
976
- stripForReveal(plain),
977
- spacePunctuation(plain)
978
- ])
979
- ]
980
- };
981
- }
982
- shownTexts.set(text, result);
983
- }
984
- return result;
985
- }
986
- };
987
- }
988
- function revealsCandidate(forms, candidate) {
989
- const memo = scopeCache(REVEALS, () => /* @__PURE__ */ new WeakMap());
990
- const known = memo?.get(forms)?.get(candidate);
991
- if (known !== void 0) {
992
- return known;
993
- }
994
- const answer = searchReveal(forms, candidate);
995
- if (memo !== void 0) {
996
- const byCandidate = memo.get(forms) ?? /* @__PURE__ */ new WeakMap();
997
- byCandidate.set(candidate, answer);
998
- memo.set(forms, byCandidate);
999
- }
1000
- return answer;
1001
- }
1002
- var COMPACTED_FORMS = /* @__PURE__ */ new WeakMap();
1003
- function searchReveal(forms, candidate) {
1004
- const { reveal } = candidate;
1005
- if (candidate.tooLong) {
1006
- return false;
1007
- }
1008
- let fields = forms;
1009
- if (reveal.long) {
1010
- fields = COMPACTED_FORMS.get(forms) ?? forms.map((form) => form.replace(SPACE_BESIDE_UNSPACED_RE, ""));
1011
- COMPACTED_FORMS.set(forms, fields);
1012
- }
1013
- return reveal.forms.some((text) => {
1014
- const phrase = { text, before: reveal.before, after: reveal.after };
1015
- return fields.some((field) => containsPhrase(field, phrase));
1016
- });
1017
- }
1018
-
1019
- // src/scoring/dictation/edit-distance.ts
1020
- function codePoints(text) {
1021
- return Array.from(text, (character) => character.codePointAt(0));
1022
- }
1023
- function editDistance(a, b) {
1024
- if (a === b) {
1025
- return 0;
1026
- }
1027
- let left = codePoints(a);
1028
- let right = codePoints(b);
1029
- if (left.length < right.length) {
1030
- [left, right] = [right, left];
1031
- }
1032
- if (right.length === 0) {
1033
- return left.length;
1034
- }
1035
- let previous = new Uint32Array(right.length + 1);
1036
- let current = new Uint32Array(right.length + 1);
1037
- for (let j = 0; j <= right.length; j += 1) {
1038
- previous[j] = j;
1039
- }
1040
- for (let i = 1; i <= left.length; i += 1) {
1041
- current[0] = i;
1042
- const leftPoint = left[i - 1];
1043
- for (let j = 1; j <= right.length; j += 1) {
1044
- const deletion = previous[j] + 1;
1045
- const insertion = current[j - 1] + 1;
1046
- const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
1047
- current[j] = Math.min(deletion, insertion, substitution);
1048
- }
1049
- [previous, current] = [current, previous];
1050
- }
1051
- return previous[right.length];
1052
- }
1053
- function similarity(a, b) {
1054
- const max = Math.max(codePoints(a).length, codePoints(b).length);
1055
- return max === 0 ? 0 : (max - editDistance(a, b)) / max;
1056
- }
1057
-
1058
- // src/scoring/dictation/align.ts
1059
- function dictationReferenceWords(data) {
1060
- return rawCandidates(data).map(
1061
- (candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
1062
- itemId: `w${index + 1}`,
1063
- word
1064
- }))
1065
- );
1066
- }
1067
- function alignDictation(data, text) {
1068
- const raw = typeof text === "string" ? text : "";
1069
- const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
1070
- const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
1071
- const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
1072
- const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
1073
- const candidates = [];
1074
- rawCandidates(data).forEach((candidate, index) => {
1075
- const candidateText = normalizeCandidate(candidate, data);
1076
- if (candidateText !== "") {
1077
- candidates.push({ index, text: candidateText });
1078
- }
1079
- });
1080
- if (candidates.length === 0) {
1081
- return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
1082
- }
1083
- let chosen = candidates[0];
1084
- let best = similarity(chosen.text, attempt);
1085
- for (const candidate of candidates.slice(1)) {
1086
- const candidateSimilarity = similarity(candidate.text, attempt);
1087
- if (candidateSimilarity > best) {
1088
- best = candidateSimilarity;
1089
- chosen = candidate;
1090
- }
1091
- }
1092
- return {
1093
- candidateIndex: chosen.index,
1094
- reference: chosen.text,
1095
- attempt,
1096
- truncated,
1097
- similarity: best,
1098
- words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
1099
- };
1100
- }
1101
- function rawCandidates(data) {
1102
- return [
1103
- data.transcript,
1104
- ...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
1105
- ];
1106
- }
1107
- function normalizeCandidate(candidate, data) {
1108
- return typeof candidate === "string" ? cutAt(
1109
- normalizeDictationText(
1110
- truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
1111
- data.tolerance
1112
- ),
1113
- DICTATION_MAX_TRANSCRIPT_LENGTH
1114
- ) : "";
1115
- }
1116
- function cutAt(normalized, max) {
1117
- const cut = truncateCodePoints(normalized, max);
1118
- return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
1119
- }
1120
- function tokensOf(normalized) {
1121
- return normalized === "" ? [] : normalized.split(" ");
1122
- }
1123
- var STEP = {
1124
- /** A reference element paired with an attempt element — equal or substituted. */
1125
- pair: 0,
1126
- /** A reference element the attempt lacks. */
1127
- missing: 1,
1128
- /** An attempt element the reference lacks. */
1129
- extra: 2
1130
- };
1131
- function alignSequences(reference, attempt) {
1132
- const rows = reference.length;
1133
- const columns = attempt.length;
1134
- const width = columns + 1;
1135
- const steps = new Uint8Array((rows + 1) * width);
1136
- let previous = new Uint32Array(width);
1137
- let current = new Uint32Array(width);
1138
- for (let j2 = 0; j2 <= columns; j2 += 1) {
1139
- previous[j2] = j2;
1140
- steps[j2] = STEP.extra;
1141
- }
1142
- for (let i2 = 1; i2 <= rows; i2 += 1) {
1143
- current[0] = i2;
1144
- steps[i2 * width] = STEP.missing;
1145
- const referenceItem = reference[i2 - 1];
1146
- for (let j2 = 1; j2 <= columns; j2 += 1) {
1147
- const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
1148
- const missing = previous[j2] + 1;
1149
- const extra = current[j2 - 1] + 1;
1150
- const cost = Math.min(paired, missing, extra);
1151
- current[j2] = cost;
1152
- steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
1153
- }
1154
- [previous, current] = [current, previous];
1155
- }
1156
- const path = [];
1157
- let i = rows;
1158
- let j = columns;
1159
- while (i > 0 || j > 0) {
1160
- const step = steps[i * width + j];
1161
- if (step === STEP.pair) {
1162
- i -= 1;
1163
- j -= 1;
1164
- } else if (step === STEP.missing) {
1165
- i -= 1;
1166
- } else {
1167
- j -= 1;
1168
- }
1169
- path.push({ step, referenceIndex: i, attemptIndex: j });
1170
- }
1171
- return path.reverse();
1172
- }
1173
- function alignWords(reference, attempt) {
1174
- const words = [];
1175
- let ordinal = 0;
1176
- for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
1177
- if (step === STEP.extra) {
1178
- words.push({
1179
- reference: "",
1180
- attempt: attempt[attemptIndex],
1181
- similarity: 0,
1182
- status: "extra"
1183
- });
1184
- continue;
1185
- }
1186
- ordinal += 1;
1187
- const referenceWord = reference[referenceIndex];
1188
- if (step === STEP.missing) {
1189
- words.push({
1190
- itemId: `w${ordinal}`,
1191
- reference: referenceWord,
1192
- attempt: "",
1193
- similarity: 0,
1194
- status: "missing"
1195
- });
1196
- continue;
1197
- }
1198
- const attemptWord = attempt[attemptIndex];
1199
- words.push({
1200
- itemId: `w${ordinal}`,
1201
- reference: referenceWord,
1202
- attempt: attemptWord,
1203
- similarity: similarity(referenceWord, attemptWord),
1204
- status: referenceWord === attemptWord ? "correct" : "incorrect"
1205
- });
1206
- }
1207
- return words;
1208
- }
1209
-
1210
- // src/errors.ts
1211
- var ActivitySchemaError = class extends Error {
1212
- constructor(activityType, errors) {
1213
- super(`Invalid activity data for type "${activityType}"`);
1214
- this.activityType = activityType;
1215
- this.errors = errors;
1216
- this.name = "ActivitySchemaError";
1217
- }
1218
- activityType;
1219
- errors;
1220
- };
1221
- var UnknownActivityTypeError = class extends Error {
1222
- constructor(activityType) {
1223
- super(`Activity type "${activityType}" is not registered`);
1224
- this.activityType = activityType;
1225
- this.name = "UnknownActivityTypeError";
1226
- }
1227
- activityType;
1228
- };
1229
- var RedactedScoringError = class extends Error {
1230
- constructor(activityType) {
1231
- super(
1232
- `Activity data for "${activityType}" carries no answer key (it looks redacted), so it cannot be scored. Score against the full activity data server-side. evaluate() answers this input without throwing, but it never grades it.`
1233
- );
1234
- this.activityType = activityType;
1235
- this.name = "RedactedScoringError";
1236
- }
1237
- activityType;
1238
- };
1239
- var DeferredScoringError = class extends Error {
1240
- constructor(activityType) {
1241
- super(
1242
- `Activity type "${activityType}" is graded asynchronously and has no synchronous score. Use evaluate() \u2014 it returns { status: 'deferred' } for this type.`
1243
- );
1244
- this.activityType = activityType;
1245
- this.name = "DeferredScoringError";
1246
- }
1247
- activityType;
1248
- };
1249
-
1250
- // src/schemas/multiple-choice.ts
1251
- import { z as z4 } from "zod/v4";
1252
-
1253
- // src/schemas/ai.ts
1254
- import { z as z2 } from "zod/v4";
1255
- var AiPermissionsSchema = z2.looseObject({
1256
- explanations: z2.boolean().optional(),
1257
- hints: z2.boolean().optional()
1258
- });
1259
-
1260
- // src/schemas/feedback.ts
1261
- import { z as z3 } from "zod/v4";
1262
- var FeedbackSchema = z3.looseObject({
1263
- correct: z3.string().min(1).optional(),
1264
- incorrect: z3.string().min(1).optional()
1265
- });
1266
-
1267
- // src/schemas/multiple-choice.ts
1268
- var MultipleChoiceOptionMediaSchema = z4.looseObject({
1269
- // No `embed`: an iframe swallows the click that selects the option, so the
1270
- // learner could not choose it. No `video`: a native control bar inside the
1271
- // option's label eats the same click.
1272
- type: z4.enum(["image", "audio"]),
1273
- url: MediaUrlSchema,
1274
- alt: z4.string().min(1).optional(),
1275
- captionsUrl: MediaUrlSchema.optional()
1276
- }).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
1277
- error: "An image option requires a non-empty `alt`.",
1278
- path: ["alt"]
1279
- }).refine((media) => !("playback" in media), {
1280
- error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
1281
- path: ["playback"]
1282
- });
1283
- var MultipleChoiceOptionSchema = z4.looseObject({
1284
- id: z4.string().min(1),
1285
- text: z4.string().min(1),
1286
- isCorrect: z4.boolean(),
1287
- feedback: z4.string().optional(),
1288
- media: MultipleChoiceOptionMediaSchema.optional()
1289
- });
1290
- var MultipleChoiceDataSchema = z4.looseObject({
1291
- schemaVersion: z4.literal("1.0"),
1292
- type: z4.literal("multiple-choice"),
1293
- id: z4.string().min(1),
1294
- title: z4.string().min(1),
1295
- question: z4.string().min(1),
1296
- questionHtml: z4.string().optional(),
1297
- mode: z4.enum(["single", "multi"]),
1298
- options: z4.array(MultipleChoiceOptionSchema).min(2).max(26),
1299
- scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
1300
- media: MediaSchema.optional(),
1301
- feedback: FeedbackSchema.optional(),
1302
- passThreshold: z4.number().min(0).max(1).optional(),
1303
- shuffle: z4.boolean().optional(),
1304
- locale: z4.string().optional(),
1305
- learningObjectives: z4.array(z4.string()).optional(),
1306
- difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional(),
1307
- ai: AiPermissionsSchema.optional()
1308
- }).refine((data) => data.options.some((option) => option.isCorrect), {
1309
- error: "At least one option must be marked correct.",
1310
- path: ["options"]
1311
- }).refine(
1312
- (data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
1313
- {
1314
- error: 'Single-select activities (mode: "single") must have exactly one correct option.',
1315
- path: ["options"]
1316
- }
1317
- ).refine((data) => new Set(data.options.map((option) => option.id)).size === data.options.length, {
1318
- error: "Option ids must be unique within the activity.",
1319
- path: ["options"]
1320
- });
1321
-
1322
- // src/scoring/speech/limits.ts
1323
- var READ_ALOUD_MAX_REFERENCE_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH;
1324
- var READ_ALOUD_MAX_SECONDS = 300;
1325
- var READ_ALOUD_MAX_TAKES = 20;
1326
- var READ_ALOUD_MAX_DIMENSION_WEIGHT = 1e3;
1327
- var SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
1328
- var SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
1329
-
1330
- // src/count-words.ts
1331
- function countWords(text) {
1332
- if (typeof text !== "string") {
1333
- return 0;
1334
- }
1335
- const trimmed = text.trim();
1336
- if (trimmed === "") {
1337
- return 0;
1338
- }
1339
- return trimmed.split(/\s+/).length;
1340
- }
1341
-
1342
- // src/schemas/read-schema.ts
1343
- var OWN_SCHEMAS = /* @__PURE__ */ new WeakSet();
1344
- function ownSchemas(...schemas) {
1345
- for (const schema of schemas) {
1346
- OWN_SCHEMAS.add(schema);
1347
- }
1348
- }
1349
- function isOwnSchema(schema) {
1350
- return OWN_SCHEMAS.has(schema);
1351
- }
1352
- var GENERIC_CODE = "invalid";
1353
- function valueAt(root, path) {
1354
- let node = root;
1355
- for (const segment of path) {
1356
- if (node === null || typeof node !== "object") {
1357
- return void 0;
1358
- }
1359
- node = node[segment];
1360
- }
1361
- return node;
1362
- }
1363
- var isThenable = (value) => typeof value?.then === "function";
1364
- function isZodSchema(schema) {
1365
- return schema["~standard"].vendor === "zod" && typeof schema.safeParse === "function";
1366
- }
1367
- function readSchema(schema, value, subject, options) {
1368
- if (isZodSchema(schema)) {
1369
- const parsed = schema.safeParse(
1370
- value,
1371
- options?.reportInput === true ? { reportInput: true } : void 0
1372
- );
1373
- if (parsed.success) {
1374
- return { success: true, data: parsed.data };
1375
- }
1376
- return {
1377
- success: false,
1378
- issues: parsed.error.issues.map((issue2) => ({
1379
- path: issue2.path,
1380
- message: issue2.message,
1381
- code: issue2.code,
1382
- ..."input" in issue2 ? { input: issue2.input } : {}
1383
- }))
1384
- };
1385
- }
1386
- const result = schema["~standard"].validate(value);
1387
- if (isThenable(result)) {
1388
- result.then(void 0, () => {
1389
- });
1390
- throw new TypeError(
1391
- `${subject} has a schema whose validation returned a Promise: it validates asynchronously, or its library reported an error that way. lk-core validates synchronously: register a schema whose validation returns its result.`
1392
- );
1393
- }
1394
- const settled = result;
1395
- if (settled.issues === void 0) {
1396
- return { success: true, data: settled.value };
1397
- }
1398
- return {
1399
- success: false,
1400
- issues: settled.issues.map((issue2) => {
1401
- const path = (issue2.path ?? []).map(
1402
- (segment) => typeof segment === "object" && segment !== null ? segment.key : segment
1403
- );
1404
- const code = issue2.code;
1405
- return {
1406
- path,
1407
- message: issue2.message,
1408
- code: typeof code === "string" && code !== "" ? code : GENERIC_CODE,
1409
- input: valueAt(value, path)
1410
- };
1411
- })
1412
- };
1413
- }
1414
- function isStandardSchema(value) {
1415
- const props = value?.["~standard"];
1416
- return props?.version === 1 && typeof props.validate === "function";
1417
- }
1418
- function standardJsonSchemaOf(schema) {
1419
- const converter = schema["~standard"].jsonSchema;
1420
- if (typeof converter?.output !== "function") {
1421
- return void 0;
1422
- }
1423
- const generated = converter.output({ target: "draft-07" });
1424
- return generated !== null && typeof generated === "object" && !Array.isArray(generated) ? generated : void 0;
1425
- }
1426
-
1427
- // src/scoring/text-match.ts
1428
- var COMBINING_MARKS_RE = /\p{M}/gu;
1429
- var PUNCTUATION_RE2 = /\p{P}/gu;
1430
- function applyBaseline(value, policy) {
1431
- let result = value;
1432
- if (policy.trim !== false) {
1433
- result = result.trim();
1434
- }
1435
- if (policy.caseSensitive !== true) {
1436
- result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
1437
- }
1438
- return result;
1439
- }
1440
- function applyNormalization(value, policy) {
1441
- let result = value;
1442
- if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
1443
- result = result.normalize(policy.normalize);
1444
- }
1445
- if (policy.ignorePunctuation === true) {
1446
- result = result.replace(PUNCTUATION_RE2, "");
1447
- }
1448
- if (policy.collapseInnerWhitespace === true) {
1449
- result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
1450
- if (policy.trim !== false) {
1451
- result = result.trim();
1452
- }
1453
- }
1454
- return result;
1455
- }
1456
- function applyDiacriticFold(value) {
1457
- return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
1458
- }
1459
- function levenshteinDistance(a, b, max) {
1460
- if (a === b) return 0;
1461
- if (Math.abs(a.length - b.length) > max) return max + 1;
1462
- if (a.length === 0) return b.length;
1463
- if (b.length === 0) return a.length;
1464
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1465
- for (let i = 1; i <= a.length; i += 1) {
1466
- const current = [i];
1467
- let rowMin = i;
1468
- for (let j = 1; j <= b.length; j += 1) {
1469
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1470
- const value = Math.min(
1471
- previous[j] + 1,
1472
- current[j - 1] + 1,
1473
- previous[j - 1] + cost
1474
- );
1475
- current.push(value);
1476
- if (value < rowMin) rowMin = value;
1477
- }
1478
- if (rowMin > max) return max + 1;
1479
- previous = current;
1480
- }
1481
- return previous[b.length];
1482
- }
1483
- function matchText(input, accepted, policy = {}) {
1484
- const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
1485
- const baselineInput = applyBaseline(input, policy);
1486
- const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
1487
- if (baselineAccepted.some((answer) => answer === baselineInput)) {
1488
- return { matched: true, via: "exact" };
1489
- }
1490
- const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
1491
- const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
1492
- const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
1493
- if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
1494
- return { matched: true, via: "normalized" };
1495
- }
1496
- const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
1497
- const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
1498
- if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
1499
- return { matched: true, via: "folded" };
1500
- }
1501
- const maxDistance = policy.levenshtein ?? 0;
1502
- if (maxDistance > 0 && foldedInput.trim().length > 0 && foldedAccepted.some(
1503
- (answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
1504
- )) {
1505
- return { matched: true, via: "fuzzy" };
1506
- }
1507
- return { matched: false, via: "none" };
1508
- }
1509
-
1510
- // src/registry/registry.ts
1511
- var registry = /* @__PURE__ */ new Map();
1512
- function defineActivityType(descriptor) {
1513
- return descriptor;
1514
- }
1515
- function registerActivityType(descriptor) {
1516
- if (descriptor.type === "item-group") {
1517
- throw new Error(
1518
- `"item-group" is reserved for the SDK's item-group container (see ItemGroup) and cannot be registered as an activity type.`
1519
- );
1520
- }
1521
- if (!isStandardSchema(descriptor.schema)) {
1522
- throw new TypeError(
1523
- `Activity type "${descriptor.type}": \`schema\` must implement Standard Schema v1 (https://standardschema.dev), as a zod 4, valibot or ArkType schema does.`
1524
- );
1525
- }
1526
- if (descriptor.redactedSchema !== void 0 && !isStandardSchema(descriptor.redactedSchema)) {
1527
- throw new TypeError(
1528
- `Activity type "${descriptor.type}": \`redactedSchema\` must implement Standard Schema v1 (https://standardschema.dev), as a zod 4, valibot or ArkType schema does.`
1529
- );
1530
- }
1531
- if (descriptor.jsonSchema !== void 0 && (descriptor.jsonSchema === null || typeof descriptor.jsonSchema !== "object" || Array.isArray(descriptor.jsonSchema))) {
1532
- throw new TypeError(
1533
- `Activity type "${descriptor.type}": \`jsonSchema\` must be a JSON Schema object.`
1534
- );
1535
- }
1536
- const existing = registry.get(descriptor.type);
1537
- if (existing !== void 0) {
1538
- if (existing === descriptor) {
1539
- return;
1540
- }
1541
- throw new Error(
1542
- `Activity type "${descriptor.type}" is already registered. Registering a different descriptor for an existing type is not allowed.`
1543
- );
1544
- }
1545
- registry.set(descriptor.type, descriptor);
1546
- }
1547
- function getActivityTypeDescriptor(type) {
1548
- return registry.get(type);
1549
- }
1550
- function registeredActivityTypes() {
1551
- return [...registry.keys()];
1552
- }
1553
-
1554
- // src/schemas/dictation.ts
1555
- import { z as z6 } from "zod/v4";
1556
-
1557
- // src/schemas/after-issues.ts
1558
- import { z as z5 } from "zod/v4";
1559
- function checkEvenAfterIssues(check) {
1560
- const guard = new z5.core.$ZodCheck({ check: "custom", when: () => true });
1561
- guard._zod.check = check;
1562
- return guard;
1563
- }
1564
- function parsedFields(payload) {
1565
- const refused = new Set(payload.issues.map((issue2) => String(issue2.path?.[0])));
1566
- const data = { ...payload.value };
1567
- for (const key of refused) {
1568
- delete data[key];
1569
- }
1570
- return { data, refused };
1571
- }
1572
-
1573
- // src/schemas/dictation.ts
1574
- var DictationSlowMediaSchema = z6.looseObject({
1575
- type: z6.literal("audio"),
1576
- url: MediaUrlSchema,
1577
- alt: z6.string().min(1).optional()
1578
- });
1579
- var RuleTextSchema = z6.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1580
- var TranscriptTextSchema = z6.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1581
- var DictationEquivalenceSchema = z6.looseObject({
1582
- from: RuleTextSchema,
1583
- to: RuleTextSchema
1584
- }).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1585
- error: `A rule rewrites at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1586
- path: ["from"]
1587
- }).refine((rule) => codePointLength(rule.to) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1588
- error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1589
- path: ["to"]
1590
- });
1591
- var DictationToleranceSchema = z6.looseObject({
1592
- equivalences: z6.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
1593
- error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
1594
- }).optional()
1595
- });
1596
- var normalizers = /* @__PURE__ */ new WeakMap();
1597
- function normalizerFor(data) {
1598
- let normalizer = normalizers.get(data);
1599
- if (normalizer === void 0) {
1600
- normalizer = dictationNormalizer(data.tolerance);
1601
- normalizers.set(data, normalizer);
1602
- }
1603
- return normalizer;
1604
- }
1605
- function measure(data, text) {
1606
- return normalizerFor(data).measure(text);
1607
- }
1608
- function candidatesOf(data) {
1609
- return [data.transcript, ...data.acceptedTranscripts ?? []];
1610
- }
1611
- function isEmpty(measured) {
1612
- return measured.normalized === "";
1613
- }
1614
- var DictationDataShape = z6.looseObject({
1615
- schemaVersion: z6.literal("1.0"),
1616
- type: z6.literal("dictation"),
1617
- id: z6.string().min(1),
1618
- title: z6.string().min(1),
1619
- transcript: TranscriptTextSchema,
1620
- acceptedTranscripts: z6.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1621
- media: MediaSchema.optional(),
1622
- slowMedia: DictationSlowMediaSchema.optional(),
1623
- hints: z6.looseObject({ mode: z6.literal("progressive-words") }).optional(),
1624
- tolerance: DictationToleranceSchema.optional(),
1625
- feedback: FeedbackSchema.optional(),
1626
- passThreshold: z6.number().min(0).max(1).optional(),
1627
- locale: z6.string().optional(),
1628
- learningObjectives: z6.array(z6.string()).optional(),
1629
- difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional(),
1630
- ai: AiPermissionsSchema.optional()
1631
- });
1632
- var DictationDataSchema = DictationDataShape.check(
1633
- checkEvenAfterIssues((ctx) => {
1634
- if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
1635
- return;
1636
- }
1637
- const { data, refused } = parsedFields(ctx);
1638
- const report = (path, input, message) => {
1639
- ctx.issues.push({ code: "custom", input, message, path: [...path] });
1640
- };
1641
- const rulesParse = !refused.has("tolerance");
1642
- const transcriptParses = !refused.has("transcript");
1643
- if (rulesParse && transcriptParses) {
1644
- const transcript = measure(data, data.transcript);
1645
- if (isEmpty(transcript)) {
1646
- report(
1647
- ["transcript"],
1648
- data.transcript,
1649
- "The transcript must contain something to type: after ignoring case, punctuation and spacing nothing is left."
1650
- );
1651
- }
1652
- if (transcript.tooLong) {
1653
- report(
1654
- ["transcript"],
1655
- data.transcript,
1656
- `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.`
1657
- );
1658
- }
1659
- if (!refused.has("acceptedTranscripts")) {
1660
- const accepted = data.acceptedTranscripts ?? [];
1661
- if (accepted.some((entry) => {
1662
- const measured = measure(data, entry);
1663
- return measured.tooLong || isEmpty(measured);
1664
- })) {
1665
- report(
1666
- ["acceptedTranscripts"],
1667
- data.acceptedTranscripts,
1668
- `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.`
1669
- );
1670
- }
1671
- const normalized = candidatesOf(data).map((entry) => measure(data, entry).normalized).filter((entry) => entry !== null);
1672
- if (new Set(normalized).size !== normalized.length) {
1673
- report(
1674
- ["acceptedTranscripts"],
1675
- data.acceptedTranscripts,
1676
- "An accepted transcript must differ from the transcript and from the other accepted transcripts once case, punctuation and spacing are ignored."
1677
- );
1678
- }
1679
- }
1680
- }
1681
- const mediaParses = !refused.has("media");
1682
- const slowParses = !refused.has("slowMedia");
1683
- if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
1684
- report(
1685
- ["media", "type"],
1686
- data.media.type,
1687
- "A dictation is dictated: its recording must be audio."
1688
- );
1689
- }
1690
- if (slowParses && data.slowMedia?.playback !== void 0) {
1691
- report(
1692
- ["slowMedia", "playback"],
1693
- data.slowMedia.playback,
1694
- "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)."
1695
- );
1696
- }
1697
- if (mediaParses && slowParses && data.slowMedia !== void 0) {
1698
- if (data.media?.playback?.maxPlays !== void 0) {
1699
- report(
1700
- ["slowMedia"],
1701
- data.slowMedia,
1702
- "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`)."
1703
- );
1704
- }
1705
- if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
1706
- report(
1707
- ["slowMedia", "url"],
1708
- data.slowMedia.url,
1709
- "The slow recording must be a different file from the recording."
1710
- );
1711
- }
1712
- if (data.media === void 0) {
1713
- report(
1714
- ["media"],
1715
- data.media,
1716
- "A slow recording accompanies a recording: add `media` first."
1717
- );
1718
- }
1719
- }
1720
- if (rulesParse && !(data.tolerance?.equivalences ?? []).every(
1721
- (rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
1722
- )) {
1723
- report(
1724
- ["tolerance", "equivalences"],
1725
- data.tolerance?.equivalences,
1726
- "An equivalence must name a word, phrase or symbol to rewrite, and rewrite it to something that survives normalisation (not only punctuation or spaces)."
1727
- );
1728
- }
1729
- for (const field of ["media", "slowMedia"]) {
1730
- const recording = data[field];
1731
- for (const captions of captionFieldsOf(recording)) {
1732
- report(
1733
- [field, captions],
1734
- recording?.[captions],
1735
- "A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
1736
- );
1737
- }
1738
- }
1739
- if (!rulesParse || !transcriptParses) {
1740
- return;
1741
- }
1742
- const normalizer = normalizerFor(data);
1743
- const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
1744
- const shown = [
1745
- [["title"], refused.has("title") ? void 0 : data.title],
1746
- [["media", "alt"], data.media?.alt],
1747
- [["slowMedia", "alt"], data.slowMedia?.alt]
1748
- ];
1749
- for (const [path, value] of shown) {
1750
- if (value === void 0) {
1751
- continue;
1752
- }
1753
- const { tooLong, forms } = normalizer.shown(value);
1754
- if (tooLong) {
1755
- report(
1756
- path,
1757
- value,
1758
- `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.`
1759
- );
1760
- } else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
1761
- report(
1762
- path,
1763
- value,
1764
- '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.'
1765
- );
1766
- }
1767
- }
1768
- })
1769
- );
1770
- var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1771
- function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void 0) {
1772
- if (!isObject(media)) {
1773
- return [];
1774
- }
1775
- const fields = [];
1776
- if (written(media.captionsUrl)) {
1777
- fields.push("captionsUrl");
1778
- }
1779
- if (Array.isArray(media.tracks) && media.tracks.length > 0) {
1780
- fields.push("tracks");
1781
- }
1782
- return fields;
1783
- }
1784
- function groupCaptionsField(group, written) {
1785
- if (!isObject(group) || !isObject(group.stimulus) || !Array.isArray(group.items)) {
1786
- return void 0;
1787
- }
1788
- const [field] = captionFieldsOf(group.stimulus.media, written);
1789
- if (field === void 0) {
1790
- return void 0;
1791
- }
1792
- const played = group.items.some(
1793
- (item) => isObject(item) && item.type === "dictation" && item.media === void 0
1794
- );
1795
- return played ? field : void 0;
1796
- }
1797
- 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.";
1798
-
1799
- // src/authoring/issues.ts
1800
- var DRAFT_ISSUE_SEVERITY = {
1801
- // Every registered type — reported by validateDraft itself
1802
- null_not_allowed: "invalid",
1803
- // Every built-in activity
1804
- schema_version_invalid: "invalid",
1805
- type_mismatch: "invalid",
1806
- id_required: "invalid",
1807
- title_required: "incomplete",
1808
- scoring_strategy_required: "incomplete",
1809
- pass_threshold_invalid: "invalid",
1810
- difficulty_level_invalid: "invalid",
1811
- ai_permissions_invalid: "invalid",
1812
- feedback_empty: "incomplete",
1813
- redacted_data: "invalid",
1814
- media_type_required: "incomplete",
1815
- media_url_required: "incomplete",
1816
- media_url_invalid: "invalid",
1817
- media_alt_required: "incomplete",
1818
- media_playback_invalid: "invalid",
1819
- media_invalid: "invalid",
1820
- // multiple-choice
1821
- mc_question_required: "incomplete",
1822
- mc_mode_required: "incomplete",
1823
- mc_options_too_few: "incomplete",
1824
- mc_options_too_many: "invalid",
1825
- mc_option_id_required: "invalid",
1826
- mc_option_id_duplicate: "invalid",
1827
- mc_option_text_required: "incomplete",
1828
- mc_option_correctness_required: "incomplete",
1829
- mc_option_media_kind: "invalid",
1830
- mc_correct_option_required: "incomplete",
1831
- mc_single_mode_one_correct: "invalid",
1832
- // fill-in-the-blanks
1833
- fib_passage_required: "incomplete",
1834
- fib_blanks_required: "incomplete",
1835
- fib_blank_id_required: "invalid",
1836
- fib_blank_id_duplicate: "invalid",
1837
- fib_accepted_answers_required: "incomplete",
1838
- fib_accepted_answer_empty: "incomplete",
1839
- fib_levenshtein_invalid: "invalid",
1840
- fib_match_locale_invalid: "invalid",
1841
- fib_match_invalid: "invalid",
1842
- fib_blank_missing: "incomplete",
1843
- fib_placeholder_missing: "incomplete",
1844
- fib_placeholder_duplicate: "invalid",
1845
- fib_blanks_mismatch: "invalid",
1846
- // item-group (the testlet container, via validateItemGroupDraft)
1847
- ig_not_an_object: "invalid",
1848
- ig_stimulus_required: "incomplete",
1849
- ig_stimulus_id_required: "invalid",
1850
- ig_stimulus_kind_required: "incomplete",
1851
- ig_stimulus_kind_invalid: "invalid",
1852
- ig_stimulus_body_required: "incomplete",
1853
- ig_stimulus_media_required: "incomplete",
1854
- ig_stimulus_media_kind: "invalid",
1855
- ig_items_required: "incomplete",
1856
- ig_item_id_required: "invalid",
1857
- ig_item_id_duplicate: "invalid",
1858
- ig_item_type_required: "incomplete",
1859
- ig_item_type_unknown: "invalid",
1860
- ig_item_nested_group: "invalid",
1861
- ig_shuffle_invalid: "invalid",
1862
- // interactive video (an item group with a timeline)
1863
- ig_timeline_stimulus_kind: "invalid",
1864
- ig_timeline_shuffle: "invalid",
1865
- ig_timeline_quiz_id_required: "incomplete",
1866
- ig_timeline_quiz_id_duplicate: "invalid",
1867
- ig_timeline_quiz_time_required: "incomplete",
1868
- ig_timeline_quiz_time_invalid: "invalid",
1869
- ig_timeline_quiz_empty: "incomplete",
1870
- ig_timeline_quiz_unknown_item: "invalid",
1871
- ig_timeline_item_duplicate: "invalid",
1872
- ig_timeline_item_unplaced: "incomplete",
1873
- ig_timeline_item_type: "invalid",
1874
- ig_timeline_dictation_media: "incomplete",
1875
- ig_timeline_chapter_order: "invalid",
1876
- ig_timeline_chapter_title: "incomplete",
1877
- // gap-select
1878
- gs_passage_required: "incomplete",
1879
- gs_gaps_required: "incomplete",
1880
- gs_gap_id_required: "invalid",
1881
- gs_gap_id_duplicate: "invalid",
1882
- gs_gap_missing: "incomplete",
1883
- gs_placeholder_missing: "incomplete",
1884
- gs_placeholder_duplicate: "invalid",
1885
- gs_gaps_mismatch: "invalid",
1886
- gs_presentation_invalid: "invalid",
1887
- gs_bank_id_required: "invalid",
1888
- gs_bank_id_duplicate: "invalid",
1889
- gs_bank_unknown: "invalid",
1890
- gs_choice_source_required: "incomplete",
1891
- gs_choice_source_conflict: "invalid",
1892
- gs_choices_too_few: "incomplete",
1893
- gs_choice_id_required: "invalid",
1894
- gs_choice_id_duplicate: "invalid",
1895
- gs_choice_text_required: "incomplete",
1896
- gs_correct_choice_required: "incomplete",
1897
- gs_correct_choice_unknown: "invalid",
1898
- // written-response
1899
- wr_prompt_required: "incomplete",
1900
- wr_min_words_required: "incomplete",
1901
- wr_min_words_invalid: "invalid",
1902
- wr_max_words_required: "incomplete",
1903
- wr_max_words_invalid: "invalid",
1904
- wr_word_bounds_order: "invalid",
1905
- wr_rubric_criteria_required: "incomplete",
1906
- wr_criterion_name_required: "incomplete",
1907
- wr_criterion_weight_required: "incomplete",
1908
- wr_criterion_weight_invalid: "invalid",
1909
- wr_rubric_weights_zero: "incomplete",
1910
- wr_rubric_weights_too_large: "invalid",
1911
- // dictation
1912
- dc_transcript_required: "incomplete",
1913
- dc_transcript_unscorable: "invalid",
1914
- dc_transcript_too_long: "invalid",
1915
- dc_accepted_transcript_empty: "incomplete",
1916
- dc_accepted_transcript_too_long: "invalid",
1917
- dc_accepted_transcript_duplicate: "invalid",
1918
- dc_accepted_transcripts_too_many: "invalid",
1919
- dc_media_kind: "invalid",
1920
- dc_captions_not_allowed: "invalid",
1921
- dc_slow_media_kind: "invalid",
1922
- dc_slow_media_playback: "invalid",
1923
- dc_slow_media_unbudgeted: "invalid",
1924
- dc_slow_media_same_url: "invalid",
1925
- dc_slow_media_without_media: "incomplete",
1926
- dc_transcript_revealed: "invalid",
1927
- dc_shown_text_too_long: "invalid",
1928
- dc_hints_mode_required: "incomplete",
1929
- dc_hints_mode_invalid: "invalid",
1930
- dc_equivalence_from_required: "incomplete",
1931
- dc_equivalence_to_required: "incomplete",
1932
- dc_tolerance_invalid: "invalid",
1933
- // read-aloud
1934
- ra_reference_text_required: "incomplete",
1935
- ra_reference_text_too_long: "invalid",
1936
- ra_reference_text_unreadable: "invalid",
1937
- ra_reference_text_unspaced_script: "invalid",
1938
- ra_locale_required: "incomplete",
1939
- ra_locale_invalid: "invalid",
1940
- ra_max_seconds_required: "incomplete",
1941
- ra_max_seconds_out_of_range: "invalid",
1942
- ra_min_seconds_out_of_range: "invalid",
1943
- ra_max_takes_out_of_range: "invalid",
1944
- ra_dimensions_required: "incomplete",
1945
- ra_dimension_name_invalid: "invalid",
1946
- ra_dimension_duplicate: "invalid",
1947
- ra_dimension_weight_invalid: "invalid",
1948
- ra_dimension_weights_zero: "invalid",
1949
- ra_media_kind: "invalid",
1950
- ra_slow_media_without_media: "incomplete",
1951
- ra_slow_media_same_recording: "invalid",
1952
- ra_slow_media_beside_play_limit: "invalid"
1953
- };
1954
- function issue(code, path, message) {
1955
- return { path: path.map(String), message, code, severity: DRAFT_ISSUE_SEVERITY[code] };
1956
- }
1957
- function isRecord(value) {
1958
- return typeof value === "object" && value !== null && !Array.isArray(value);
1959
- }
1960
- function valueAt2(root, path) {
1961
- let node = root;
1962
- for (const segment of path) {
1963
- if (typeof node !== "object" || node === null) {
1964
- return void 0;
1965
- }
1966
- node = node[segment];
1967
- }
1968
- return node;
1969
- }
1970
- function refusesEmpty(root, schemaIssue) {
1971
- const { path } = schemaIssue;
1972
- const value = valueAt2(root, path);
1973
- const index = path.at(-1);
1974
- const list = valueAt2(root, path.slice(0, -1));
1975
- const empty = value === null || value === void 0 && typeof index === "number" && Array.isArray(list) && index < list.length;
1976
- return empty && (schemaIssue.input === value || schemaIssue.code === "invalid_union");
1977
- }
1978
- function isUnset(value) {
1979
- return value === void 0 || value === null;
1980
- }
1981
- function isUnwritten(value) {
1982
- return isUnset(value) || typeof value === "string" && value.trim() === "";
1983
- }
1984
- function isMissingId(value) {
1985
- return isUnset(value) || value === "";
1986
- }
1987
- function isWholeNumber(value) {
1988
- return typeof value === "number" && Number.isSafeInteger(value);
1989
- }
1990
- function isTooLarge(value) {
1991
- return typeof value === "number" && value > Number.MAX_SAFE_INTEGER && Number.isInteger(value);
1992
- }
1993
- function checkIdentity(draft, type) {
1994
- const issues = [];
1995
- if (draft.schemaVersion !== "1.0") {
1996
- issues.push(
1997
- issue(
1998
- "schema_version_invalid",
1999
- ["schemaVersion"],
2000
- 'The draft must have schemaVersion "1.0". A draft from createDraft has it.'
2001
- )
2002
- );
2003
- }
2004
- if (draft.type !== type) {
2005
- issues.push(issue("type_mismatch", ["type"], `The draft's type must be "${type}".`));
2006
- }
2007
- if (isMissingId(draft.id)) {
2008
- issues.push(issue("id_required", ["id"], "The activity has no id."));
2009
- }
2010
- if (isUnwritten(draft.title)) {
2011
- issues.push(issue("title_required", ["title"], "Add a title."));
2012
- }
2013
- return issues;
2014
- }
2015
- function checkScoringStrategy(draft) {
2016
- return isUnwritten(draft.scoringStrategy) ? [
2017
- issue(
2018
- "scoring_strategy_required",
2019
- ["scoringStrategy"],
2020
- "Choose how the question is scored."
2021
- )
2022
- ] : [];
2023
- }
2024
- var DIFFICULTY_LEVELS = [1, 2, 3, 4, 5];
2025
- function checkSharedOptional(draft) {
2026
- const issues = [];
2027
- const threshold = draft.passThreshold;
2028
- if (!isUnset(threshold) && !(typeof threshold === "number" && threshold >= 0 && threshold <= 1)) {
2029
- issues.push(
2030
- issue(
2031
- "pass_threshold_invalid",
2032
- ["passThreshold"],
2033
- "The pass threshold must be a number from 0 to 1."
2034
- )
2035
- );
2036
- }
2037
- if (!isUnset(draft.difficultyLevel) && !DIFFICULTY_LEVELS.includes(draft.difficultyLevel)) {
2038
- issues.push(
2039
- issue(
2040
- "difficulty_level_invalid",
2041
- ["difficultyLevel"],
2042
- "The difficulty level must be a whole number from 1 to 5."
2043
- )
2044
- );
2045
- }
2046
- const ai = draft.ai;
2047
- if (!isUnset(ai)) {
2048
- if (!isRecord(ai)) {
2049
- issues.push(
2050
- issue(
2051
- "ai_permissions_invalid",
2052
- ["ai"],
2053
- "AI permissions must be an object of on/off flags, such as { hints: false }."
2054
- )
2055
- );
2056
- } else {
2057
- for (const key of ["explanations", "hints"]) {
2058
- if (!isUnset(ai[key]) && typeof ai[key] !== "boolean") {
2059
- issues.push(
2060
- issue(
2061
- "ai_permissions_invalid",
2062
- ["ai", key],
2063
- "Set this to true or false, or remove it."
2064
- )
2065
- );
2066
- }
2067
- }
2068
- }
2069
- }
2070
- const feedback = draft.feedback;
2071
- if (isRecord(feedback)) {
2072
- for (const key of ["correct", "incorrect"]) {
2073
- const message = feedback[key];
2074
- if (typeof message === "string" && message.trim() === "") {
2075
- issues.push(
2076
- issue("feedback_empty", ["feedback", key], "Write this feedback message, or remove it.")
2077
- );
2078
- }
2079
- }
2080
- }
2081
- if (draft.redacted === true) {
2082
- issues.push(
2083
- issue(
2084
- "redacted_data",
2085
- ["redacted"],
2086
- "This is a copy prepared for learners, with the answer key removed. Edit the original activity instead."
2087
- )
2088
- );
2089
- }
2090
- if (isRecord(draft.media)) {
2091
- issues.push(...checkMedia(draft.media));
2092
- }
2093
- return issues;
2094
- }
2095
- var URL_POLICY = 'Use an https:, http:, data: or blob: address, or a path on the same site that starts with a single "/".';
2096
- var EMBED_URL = "An embed needs the provider's full http(s) embed address, such as https://www.youtube.com/embed/VIDEO_ID.";
2097
- function checkMedia(media, prefix = ["media"], schema = MediaSchema) {
2098
- const issues = [];
2099
- const at = (...rest) => [...prefix, ...rest];
2100
- const parsed = schema.safeParse(media, { reportInput: true });
2101
- const schemaIssues = parsed.success ? [] : parsed.error.issues.filter((found) => !refusesEmpty(media, found));
2102
- const refusal = (field) => schemaIssues.find((found) => found.path[0] === field);
2103
- const typeUnset = isUnwritten(media.type);
2104
- if (typeUnset) {
2105
- issues.push(issue("media_type_required", at("type"), "Choose what kind of media this is."));
2106
- }
2107
- if (isUnwritten(media.url)) {
2108
- issues.push(issue("media_url_required", at("url"), "Add the address of the media file."));
2109
- } else {
2110
- const refused = refusal("url");
2111
- if (refused !== void 0) {
2112
- issues.push(
2113
- issue("media_url_invalid", at("url"), refused.code === "custom" ? EMBED_URL : URL_POLICY)
2114
- );
2115
- }
2116
- }
2117
- const needsAlt = media.type === "image" || media.type === "embed";
2118
- const alt = media.alt;
2119
- if (needsAlt && isUnwritten(alt) || typeof alt === "string" && alt.trim() === "") {
2120
- issues.push(issue("media_alt_required", at("alt"), "Add a text description of the media."));
2121
- }
2122
- const captions = media.captionsUrl;
2123
- if (typeof captions === "string" && captions.trim() === "") {
2124
- issues.push(
2125
- issue(
2126
- "media_url_required",
2127
- at("captionsUrl"),
2128
- "Add the address of the captions file, or remove the captions."
2129
- )
2130
- );
2131
- } else if (refusal("captionsUrl") !== void 0) {
2132
- issues.push(issue("media_url_invalid", at("captionsUrl"), URL_POLICY));
2133
- }
2134
- for (const schemaIssue of schemaIssues) {
2135
- const field = schemaIssue.path[0];
2136
- if (field === "url" || field === "alt" || field === "captionsUrl" || field === "type" && typeUnset) {
2137
- continue;
2138
- }
2139
- issues.push(
2140
- issue(
2141
- field === "playback" ? "media_playback_invalid" : "media_invalid",
2142
- at(...schemaIssue.path.map(String)),
2143
- schemaIssue.message
2144
- )
2145
- );
2146
- }
2147
- return issues;
2148
- }
2149
- function covers(reported, failed) {
2150
- if (failed.length === 0) {
2151
- return reported.length === 0;
2152
- }
2153
- return failed.every((segment, index) => reported[index] === segment);
2154
- }
2155
- function pathKey(path) {
2156
- return path.length === 0 ? "" : JSON.stringify(path.map(String));
2157
- }
2158
- function coveredPathsOf(reported) {
2159
- const covered = /* @__PURE__ */ new Set();
2160
- for (const known of reported) {
2161
- if (known.path.length === 0) {
2162
- covered.add("");
2163
- }
2164
- for (let length = 1; length <= known.path.length; length += 1) {
2165
- covered.add(pathKey(known.path.slice(0, length)));
2166
- }
2167
- }
2168
- return covered;
2169
- }
2170
-
2171
- // src/authoring/dictation.ts
2172
- var dictationAuthoring = {
2173
- createDraft: ({ newId }) => ({
2174
- schemaVersion: "1.0",
2175
- type: "dictation",
2176
- id: newId(),
2177
- title: "",
2178
- transcript: ""
2179
- }),
2180
- checkDraft: checkDictationDraft
2181
- };
2182
- function checkDictationDraft(draft) {
2183
- const issues = checkIdentity(draft, "dictation");
2184
- const tolerance = isRecord(draft.tolerance) ? draft.tolerance : void 0;
2185
- const normalizer = dictationNormalizer(tolerance);
2186
- issues.push(...checkTranscript(draft.transcript, normalizer));
2187
- issues.push(...checkAcceptedTranscripts(draft, normalizer));
2188
- issues.push(...checkRecording(draft.media));
2189
- issues.push(...checkSlowRecording(draft));
2190
- issues.push(...checkRevealed(draft, normalizer));
2191
- issues.push(...checkHints(draft.hints));
2192
- issues.push(...checkTolerance(draft.tolerance));
2193
- issues.push(...checkSharedOptional(draft));
2194
- return issues;
2195
- }
2196
- function isUnwrittenTranscript(value) {
2197
- if (typeof value === "string" && isLongerThan(value, DICTATION_MAX_TRANSCRIPT_LENGTH)) {
2198
- return false;
2199
- }
2200
- return isUnwritten(value) || typeof value === "string" && isBlankBeforeRules(value);
2201
- }
2202
- function acceptedTranscriptsRead(accepted) {
2203
- return accepted.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS);
2204
- }
2205
- function checkTranscript(transcript, normalizer) {
2206
- if (isUnwrittenTranscript(transcript)) {
2207
- return [issue("dc_transcript_required", ["transcript"], "Write the transcript.")];
2208
- }
2209
- if (typeof transcript !== "string") {
2210
- return [];
2211
- }
2212
- const measured = normalizer.measure(transcript);
2213
- if (measured.tooLong) {
2214
- return [
2215
- issue(
2216
- "dc_transcript_too_long",
2217
- ["transcript"],
2218
- `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.`
2219
- )
2220
- ];
2221
- }
2222
- if (measured.normalized === "") {
2223
- return [
2224
- issue(
2225
- "dc_transcript_unscorable",
2226
- ["transcript"],
2227
- "The transcript has nothing to type: once case, punctuation and spacing are ignored, nothing is left."
2228
- )
2229
- ];
2230
- }
2231
- return [];
2232
- }
2233
- function checkAcceptedTranscripts(draft, normalizer) {
2234
- const accepted = draft.acceptedTranscripts;
2235
- if (!Array.isArray(accepted)) {
2236
- return [];
2237
- }
2238
- const issues = [];
2239
- if (accepted.length > DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
2240
- issues.push(
2241
- issue(
2242
- "dc_accepted_transcripts_too_many",
2243
- ["acceptedTranscripts"],
2244
- `At most ${DICTATION_MAX_ACCEPTED_TRANSCRIPTS} accepted transcripts. Prefer an equivalence for a word that varies.`
2245
- )
2246
- );
2247
- }
2248
- for (const [index, text] of accepted.entries()) {
2249
- if (typeof text !== "string") {
2250
- continue;
2251
- }
2252
- if (index >= DICTATION_MAX_ACCEPTED_TRANSCRIPTS) {
2253
- if (text === "") {
2254
- issues.push(acceptedTranscriptEmpty(index));
2255
- }
2256
- continue;
2257
- }
2258
- const measured = normalizer.measure(text);
2259
- if (measured.tooLong) {
2260
- issues.push(
2261
- issue(
2262
- "dc_accepted_transcript_too_long",
2263
- ["acceptedTranscripts", index],
2264
- `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.`
2265
- )
2266
- );
2267
- } else if (measured.normalized === "") {
2268
- issues.push(acceptedTranscriptEmpty(index));
2269
- }
2270
- }
2271
- const candidates = [draft.transcript, ...acceptedTranscriptsRead(accepted)].filter((text) => typeof text === "string").map((text) => normalizer.measure(text).normalized).filter((text) => text !== null && text !== "");
2272
- if (new Set(candidates).size !== candidates.length) {
2273
- issues.push(
2274
- issue(
2275
- "dc_accepted_transcript_duplicate",
2276
- ["acceptedTranscripts"],
2277
- "An accepted transcript repeats the transcript, or another accepted transcript, once case, punctuation and spacing are ignored. Remove it."
2278
- )
2279
- );
2280
- }
2281
- return issues;
2282
- }
2283
- function acceptedTranscriptEmpty(index) {
2284
- return issue(
2285
- "dc_accepted_transcript_empty",
2286
- ["acceptedTranscripts", index],
2287
- `Accepted transcript ${index + 1} has nothing to type. Fill it in or remove it.`
2288
- );
2289
- }
2290
- function checkRecording(media) {
2291
- if (!isRecord(media)) {
2292
- return [];
2293
- }
2294
- const issues = [];
2295
- const kind = media.type;
2296
- if (!isUnwritten(kind) && kind !== "audio") {
2297
- issues.push(
2298
- issue(
2299
- "dc_media_kind",
2300
- ["media", "type"],
2301
- "A dictation is dictated: its recording must be audio."
2302
- )
2303
- );
2304
- }
2305
- for (const captions of captionFieldsOf(media, (captionsUrl) => !isUnset(captionsUrl))) {
2306
- issues.push(issue("dc_captions_not_allowed", ["media", captions], CAPTIONS_ARE_THE_ANSWER));
2307
- }
2308
- return issues;
2309
- }
2310
- 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.";
2311
- function checkSlowRecording(draft) {
2312
- const slow = draft.slowMedia;
2313
- if (!isRecord(slow)) {
2314
- return [];
2315
- }
2316
- const issues = [];
2317
- const kind = slow.type;
2318
- const wrongKind = !isUnwritten(kind) && kind !== "audio";
2319
- if (wrongKind) {
2320
- issues.push(
2321
- issue("dc_slow_media_kind", ["slowMedia", "type"], "The slow recording must be audio.")
2322
- );
2323
- }
2324
- issues.push(
2325
- ...checkMedia(
2326
- wrongKind ? { ...slow, type: "audio" } : slow,
2327
- ["slowMedia"],
2328
- DictationSlowMediaSchema
2329
- )
2330
- );
2331
- for (const captions of captionFieldsOf(slow, (captionsUrl) => !isUnset(captionsUrl))) {
2332
- issues.push(issue("dc_captions_not_allowed", ["slowMedia", captions], CAPTIONS_ARE_THE_ANSWER));
2333
- }
2334
- if (slow.playback !== void 0) {
2335
- issues.push(
2336
- issue(
2337
- "dc_slow_media_playback",
2338
- ["slowMedia", "playback"],
2339
- "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."
2340
- )
2341
- );
2342
- }
2343
- const media = draft.media;
2344
- if (isUnset(media)) {
2345
- issues.push(
2346
- issue(
2347
- "dc_slow_media_without_media",
2348
- ["media"],
2349
- "A slow recording accompanies a recording. Add the recording first."
2350
- )
2351
- );
2352
- return issues;
2353
- }
2354
- if (!isRecord(media)) {
2355
- return issues;
2356
- }
2357
- if (isRecord(media.playback) && media.playback.maxPlays !== void 0) {
2358
- issues.push(
2359
- issue(
2360
- "dc_slow_media_unbudgeted",
2361
- ["slowMedia"],
2362
- "A play budget on the recording cannot coexist with a slow recording that has none. Remove the slow recording, or remove the play limit."
2363
- )
2364
- );
2365
- }
2366
- if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) {
2367
- issues.push(
2368
- issue(
2369
- "dc_slow_media_same_url",
2370
- ["slowMedia", "url"],
2371
- "The slow recording is the same file as the recording. Point it at the slower render."
2372
- )
2373
- );
2374
- }
2375
- return issues;
2376
- }
2377
- function checkRevealed(draft, normalizer) {
2378
- const candidates = [
2379
- draft.transcript,
2380
- ...Array.isArray(draft.acceptedTranscripts) ? acceptedTranscriptsRead(draft.acceptedTranscripts) : []
2381
- ].filter((text) => typeof text === "string").map((text) => normalizer.measure(text));
2382
- const shown = [
2383
- [["title"], draft.title],
2384
- [["media", "alt"], isRecord(draft.media) ? draft.media.alt : void 0],
2385
- [["slowMedia", "alt"], isRecord(draft.slowMedia) ? draft.slowMedia.alt : void 0]
2386
- ];
2387
- const issues = [];
2388
- for (const [path, text] of shown) {
2389
- if (typeof text !== "string") {
2390
- continue;
2391
- }
2392
- const { tooLong, forms } = normalizer.shown(text);
2393
- if (tooLong) {
2394
- issues.push(
2395
- issue(
2396
- "dc_shown_text_too_long",
2397
- path,
2398
- `${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.`
2399
- )
2400
- );
2401
- } else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
2402
- issues.push(
2403
- issue(
2404
- "dc_transcript_revealed",
2405
- path,
2406
- 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.'
2407
- )
2408
- );
2409
- }
2410
- }
2411
- return issues;
2412
- }
2413
- function checkHints(hints) {
2414
- if (!isRecord(hints)) {
2415
- return [];
2416
- }
2417
- const mode = hints.mode;
2418
- if (isUnwritten(mode)) {
2419
- return [issue("dc_hints_mode_required", ["hints", "mode"], "Choose how hints are given.")];
2420
- }
2421
- if (mode !== "progressive-words") {
2422
- return [
2423
- issue(
2424
- "dc_hints_mode_invalid",
2425
- ["hints", "mode"],
2426
- 'The only hint mode is "progressive-words". Leave hints out unless you mean to offer them.'
2427
- )
2428
- ];
2429
- }
2430
- return [];
2431
- }
2432
- function checkTolerance(tolerance) {
2433
- if (isUnset(tolerance)) {
2434
- return [];
2435
- }
2436
- if (!isRecord(tolerance)) {
2437
- return [
2438
- issue(
2439
- "dc_tolerance_invalid",
2440
- ["tolerance"],
2441
- "The tolerances are an object: `{ equivalences: [{ from, to }] }`."
2442
- )
2443
- ];
2444
- }
2445
- const issues = [];
2446
- const rules = tolerance.equivalences;
2447
- if (Array.isArray(rules)) {
2448
- if (rules.length > DICTATION_MAX_EQUIVALENCES) {
2449
- issues.push(
2450
- issue(
2451
- "dc_tolerance_invalid",
2452
- ["tolerance", "equivalences"],
2453
- `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
2454
- )
2455
- );
2456
- }
2457
- for (const [index, rule] of rules.entries()) {
2458
- if (!isRecord(rule)) {
2459
- continue;
2460
- }
2461
- const path = ["tolerance", "equivalences", index];
2462
- const from = rule.from;
2463
- if (isUnwritten(from) || typeof from === "string" && preStripNormalize(from) === "") {
2464
- issues.push(
2465
- issue(
2466
- "dc_equivalence_from_required",
2467
- [...path, "from"],
2468
- `Say what rule ${index + 1} rewrites: a word, a phrase or a symbol.`
2469
- )
2470
- );
2471
- }
2472
- const to = rule.to;
2473
- if (isUnwritten(to) || typeof to === "string" && normalizeDictationText(to) === "") {
2474
- issues.push(
2475
- issue(
2476
- "dc_equivalence_to_required",
2477
- [...path, "to"],
2478
- `Say what rule ${index + 1} rewrites to. A rule cannot delete a word or leave only punctuation.`
2479
- )
2480
- );
2481
- }
2482
- }
2483
- }
2484
- const parsed = DictationToleranceSchema.safeParse(tolerance, { reportInput: true });
2485
- if (!parsed.success) {
2486
- const covered = coveredPathsOf(issues);
2487
- for (const schemaIssue of parsed.error.issues) {
2488
- const path = ["tolerance", ...schemaIssue.path.map(String)];
2489
- if (refusesEmpty(tolerance, schemaIssue) || covered.has(pathKey(path))) {
2490
- continue;
2491
- }
2492
- issues.push(issue("dc_tolerance_invalid", path, schemaIssue.message));
2493
- for (const key of coveredPathsOf([{ path }])) {
2494
- covered.add(key);
2495
- }
2496
- }
2497
- }
2498
- return issues;
2499
- }
2500
-
2501
- // src/schemas/fill-in-the-blanks.ts
2502
- import { z as z7 } from "zod/v4";
2503
- var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
2504
- var TextMatchPolicySchema = z7.looseObject({
2505
- caseSensitive: z7.boolean().optional(),
2506
- trim: z7.boolean().optional(),
2507
- normalize: z7.enum(["none", "NFC", "NFKC"]).optional(),
2508
- foldDiacritics: z7.boolean().optional(),
2509
- collapseInnerWhitespace: z7.boolean().optional(),
2510
- ignorePunctuation: z7.boolean().optional(),
2511
- levenshtein: z7.number().int().min(0).optional(),
2512
- locale: z7.string().optional()
2513
- });
2514
- var BlankConfigSchema = z7.looseObject({
2515
- id: z7.string().min(1),
2516
- acceptedAnswers: z7.array(
2517
- z7.string().min(1).refine((answer) => answer.trim().length > 0, {
2518
- error: "Accepted answers must contain non-whitespace characters."
2519
- })
2520
- ).min(1),
2521
- caseSensitive: z7.boolean().optional(),
2522
- trimWhitespace: z7.boolean().optional(),
2523
- match: TextMatchPolicySchema.optional(),
2524
- hint: z7.string().optional(),
2525
- feedback: z7.string().optional()
2526
- });
2527
- var FillInTheBlanksDataSchema = z7.looseObject({
2528
- schemaVersion: z7.literal("1.0"),
2529
- type: z7.literal("fill-in-the-blanks"),
2530
- id: z7.string().min(1),
2531
- title: z7.string().min(1),
2532
- passage: z7.string().min(1),
2533
- passageHtml: z7.string().optional(),
2534
- blanks: z7.array(BlankConfigSchema).min(1),
2535
- scoringStrategy: z7.enum(["all-or-nothing", "partial"]),
2536
- media: MediaSchema.optional(),
2537
- feedback: FeedbackSchema.optional(),
2538
- passThreshold: z7.number().min(0).max(1).optional(),
2539
- locale: z7.string().optional(),
2540
- learningObjectives: z7.array(z7.string()).optional(),
2541
- difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional(),
2542
- ai: AiPermissionsSchema.optional()
2543
- }).refine(
2544
- (data) => {
2545
- const placeholderCounts2 = /* @__PURE__ */ new Map();
2546
- for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
2547
- const id = match[1];
2548
- placeholderCounts2.set(id, (placeholderCounts2.get(id) ?? 0) + 1);
2549
- }
2550
- const blankIds = data.blanks.map((blank) => blank.id);
2551
- if (new Set(blankIds).size !== blankIds.length) {
2552
- return false;
2553
- }
2554
- if (placeholderCounts2.size !== blankIds.length) {
2555
- return false;
2556
- }
2557
- return blankIds.every((id) => placeholderCounts2.get(id) === 1);
2558
- },
2559
- {
2560
- error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
2561
- path: ["passage"]
2562
- }
2563
- );
2564
-
2565
- // src/authoring/fill-in-the-blanks.ts
2566
- var fillInTheBlanksAuthoring = {
2567
- createDraft: ({ newId }) => ({
2568
- schemaVersion: "1.0",
2569
- type: "fill-in-the-blanks",
2570
- id: newId(),
2571
- title: "",
2572
- passage: "",
2573
- blanks: [],
2574
- scoringStrategy: "all-or-nothing"
2575
- }),
2576
- checkDraft: checkFillInTheBlanksDraft
2577
- };
2578
- function checkFillInTheBlanksDraft(draft) {
2579
- const issues = checkIdentity(draft, "fill-in-the-blanks");
2580
- const passage = draft.passage;
2581
- if (isUnwritten(passage)) {
2582
- issues.push(issue("fib_passage_required", ["passage"], "Write the passage."));
2583
- }
2584
- issues.push(...checkScoringStrategy(draft));
2585
- const blanks = isUnset(draft.blanks) ? [] : draft.blanks;
2586
- if (Array.isArray(blanks)) {
2587
- if (blanks.length === 0) {
2588
- issues.push(
2589
- issue(
2590
- "fib_blanks_required",
2591
- ["blanks"],
2592
- "Add at least one blank, and mark where it goes in the passage with {{id}}."
2593
- )
2594
- );
2595
- }
2596
- const ids = [];
2597
- for (const [index, blank] of blanks.entries()) {
2598
- if (!isRecord(blank)) {
2599
- continue;
2600
- }
2601
- const name = typeof blank.id === "string" && blank.id !== "" ? `"${blank.id}"` : index + 1;
2602
- if (isMissingId(blank.id)) {
2603
- issues.push(
2604
- issue("fib_blank_id_required", ["blanks", index, "id"], `Blank ${index + 1} has no id.`)
2605
- );
2606
- } else if (typeof blank.id === "string") {
2607
- ids.push(blank.id);
2608
- }
2609
- const answers = isUnset(blank.acceptedAnswers) ? [] : blank.acceptedAnswers;
2610
- if (Array.isArray(answers)) {
2611
- if (answers.length === 0) {
2612
- issues.push(
2613
- issue(
2614
- "fib_accepted_answers_required",
2615
- ["blanks", index, "acceptedAnswers"],
2616
- `Add an accepted answer for blank ${name}.`
2617
- )
2618
- );
2619
- }
2620
- for (const [position, answer] of answers.entries()) {
2621
- if (typeof answer === "string" && answer.trim() === "") {
2622
- issues.push(
2623
- issue(
2624
- "fib_accepted_answer_empty",
2625
- ["blanks", index, "acceptedAnswers", position],
2626
- `An accepted answer for blank ${name} is empty. Fill it in or remove it.`
2627
- )
2628
- );
2629
- }
2630
- }
2631
- }
2632
- if (isRecord(blank.match)) {
2633
- issues.push(...checkMatch(blank.match, index));
2634
- }
2635
- }
2636
- if (typeof passage === "string" && passage.trim() !== "") {
2637
- issues.push(...checkPairing(passage, blanks, ids));
2638
- }
2639
- }
2640
- issues.push(...checkSharedOptional(draft));
2641
- return issues;
2642
- }
2643
- function checkMatch(match, index) {
2644
- const issues = [];
2645
- const at = (...rest) => ["blanks", index, "match", ...rest];
2646
- const tolerance = match.levenshtein;
2647
- if (!isUnset(tolerance) && !(isWholeNumber(tolerance) && tolerance >= 0)) {
2648
- issues.push(
2649
- issue(
2650
- "fib_levenshtein_invalid",
2651
- at("levenshtein"),
2652
- isTooLarge(tolerance) ? "Typo tolerance is too large." : "Typo tolerance must be a whole number, 0 or more."
2653
- )
2654
- );
2655
- }
2656
- const locale = match.locale;
2657
- if (typeof locale === "string" && locale !== "" && !isLanguageTag(locale)) {
2658
- issues.push(
2659
- issue(
2660
- "fib_match_locale_invalid",
2661
- at("locale"),
2662
- `"${locale}" is not a language tag. Use one such as "tr" or "en-US", or leave the locale out.`
2663
- )
2664
- );
2665
- }
2666
- const parsed = TextMatchPolicySchema.safeParse(match, { reportInput: true });
2667
- if (!parsed.success) {
2668
- for (const schemaIssue of parsed.error.issues) {
2669
- if (schemaIssue.path[0] === "levenshtein" || refusesEmpty(match, schemaIssue)) {
2670
- continue;
2671
- }
2672
- issues.push(
2673
- issue("fib_match_invalid", at(...schemaIssue.path.map(String)), schemaIssue.message)
2674
- );
2675
- }
2676
- }
2677
- return issues;
2678
- }
2679
- function isLanguageTag(tag) {
2680
- try {
2681
- Intl.getCanonicalLocales(tag);
2682
- return true;
2683
- } catch {
2684
- return false;
2685
- }
2686
- }
2687
- function checkPairing(passage, blanks, ids) {
2688
- const issues = [];
2689
- const placeholders = /* @__PURE__ */ new Map();
2690
- for (const match of passage.matchAll(PLACEHOLDER_RE)) {
2691
- const id = match[1];
2692
- placeholders.set(id, (placeholders.get(id) ?? 0) + 1);
2693
- }
2694
- const blankIds = /* @__PURE__ */ new Map();
2695
- for (const id of ids) {
2696
- blankIds.set(id, (blankIds.get(id) ?? 0) + 1);
2697
- }
2698
- for (const [id, count] of blankIds) {
2699
- if (count > 1) {
2700
- issues.push(
2701
- issue("fib_blank_id_duplicate", ["passage"], `More than one blank has the id "${id}".`)
2702
- );
2703
- }
2704
- }
2705
- for (const [id, count] of placeholders) {
2706
- if (count > 1) {
2707
- issues.push(
2708
- issue(
2709
- "fib_placeholder_duplicate",
2710
- ["passage"],
2711
- `{{${id}}} appears more than once in the passage. Each blank goes in one place.`
2712
- )
2713
- );
2714
- }
2715
- if (!blankIds.has(id)) {
2716
- issues.push(
2717
- issue(
2718
- "fib_blank_missing",
2719
- ["passage"],
2720
- `The passage has {{${id}}}, but there is no blank with that id.`
2721
- )
2722
- );
2723
- }
2724
- }
2725
- for (const id of blankIds.keys()) {
2726
- if (!placeholders.has(id)) {
2727
- issues.push(
2728
- issue(
2729
- "fib_placeholder_missing",
2730
- ["passage"],
2731
- `Blank "${id}" is not in the passage. Put {{${id}}} where it goes.`
2732
- )
2733
- );
2734
- }
2735
- }
2736
- if (issues.length === 0 && !pairsOneToOne(placeholders, blanks)) {
2737
- issues.push(
2738
- issue(
2739
- "fib_blanks_mismatch",
2740
- ["passage"],
2741
- "The blanks and the {{id}} placeholders in the passage do not pair up one to one."
2742
- )
2743
- );
2744
- }
2745
- return issues;
2746
- }
2747
- function pairsOneToOne(placeholders, blanks) {
2748
- const blankIds = blanks.filter(isRecord).map((blank) => blank.id);
2749
- if (new Set(blankIds).size !== blankIds.length) {
2750
- return false;
2751
- }
2752
- if (placeholders.size !== blankIds.length) {
2753
- return false;
2754
- }
2755
- return blankIds.every((id) => typeof id === "string" && placeholders.get(id) === 1);
2756
- }
2757
-
2758
- // src/authoring/gap-select.ts
2759
- var MIN_CHOICES = 2;
2760
- var gapSelectAuthoring = {
2761
- createDraft: ({ newId }) => ({
2762
- schemaVersion: "1.0",
2763
- type: "gap-select",
2764
- id: newId(),
2765
- title: "",
2766
- passage: "",
2767
- gaps: [],
2768
- scoringStrategy: "all-or-nothing"
2769
- }),
2770
- checkDraft: checkGapSelectDraft
2771
- };
2772
- function checkGapSelectDraft(draft) {
2773
- const issues = checkIdentity(draft, "gap-select");
2774
- const passage = draft.passage;
2775
- if (isUnwritten(passage)) {
2776
- issues.push(issue("gs_passage_required", ["passage"], "Write the passage."));
2777
- }
2778
- issues.push(...checkScoringStrategy(draft));
2779
- if (!isUnset(draft.presentation) && draft.presentation !== "dropdown") {
2780
- issues.push(
2781
- issue(
2782
- "gs_presentation_invalid",
2783
- ["presentation"],
2784
- 'The only presentation is "dropdown". Leave it out unless you mean to set it.'
2785
- )
2786
- );
2787
- }
2788
- const banks = isUnset(draft.banks) ? [] : draft.banks;
2789
- const bankIds = /* @__PURE__ */ new Set();
2790
- if (Array.isArray(banks)) {
2791
- issues.push(...checkBanks(banks, bankIds));
2792
- }
2793
- const gaps = isUnset(draft.gaps) ? [] : draft.gaps;
2794
- const gapIds = [];
2795
- if (Array.isArray(gaps)) {
2796
- if (gaps.length === 0) {
2797
- issues.push(
2798
- issue(
2799
- "gs_gaps_required",
2800
- ["gaps"],
2801
- "Add at least one gap, and mark where it goes in the passage with {{id}}."
2802
- )
2803
- );
2804
- }
2805
- issues.push(...checkGaps(gaps, banks, bankIds, gapIds));
2806
- if (typeof passage === "string" && passage.trim() !== "") {
2807
- issues.push(...checkPairing2(passage, gaps, gapIds));
2808
- }
2809
- }
2810
- issues.push(...checkSharedOptional(draft));
2811
- return issues;
2812
- }
2813
- function checkBanks(banks, bankIds) {
2814
- const issues = [];
2815
- const repeated = /* @__PURE__ */ new Set();
2816
- for (const [index, bank] of banks.entries()) {
2817
- if (!isRecord(bank)) {
2818
- continue;
2819
- }
2820
- if (isMissingId(bank.id)) {
2821
- issues.push(
2822
- issue("gs_bank_id_required", ["banks", index, "id"], `Word bank ${index + 1} has no id.`)
2823
- );
2824
- } else if (typeof bank.id === "string") {
2825
- if (bankIds.has(bank.id)) {
2826
- repeated.add(bank.id);
2827
- }
2828
- bankIds.add(bank.id);
2829
- }
2830
- const name = typeof bank.id === "string" && bank.id !== "" ? `"${bank.id}"` : index + 1;
2831
- issues.push(...checkChoiceList(bank.choices, ["banks", index, "choices"], `word bank ${name}`));
2832
- }
2833
- for (const id of repeated) {
2834
- issues.push(
2835
- issue("gs_bank_id_duplicate", ["banks"], `More than one word bank has the id "${id}".`)
2836
- );
2837
- }
2838
- return issues;
2839
- }
2840
- function checkGaps(gaps, banks, bankIds, gapIds) {
2841
- const issues = [];
2842
- const atGaps = [];
2843
- const repeated = /* @__PURE__ */ new Set();
2844
- const seen = /* @__PURE__ */ new Set();
2845
- for (const [index, gap] of gaps.entries()) {
2846
- if (!isRecord(gap)) {
2847
- continue;
2848
- }
2849
- const ordinal = index + 1;
2850
- const name = typeof gap.id === "string" && gap.id !== "" ? `"${gap.id}"` : ordinal;
2851
- if (isMissingId(gap.id)) {
2852
- issues.push(issue("gs_gap_id_required", ["gaps", index, "id"], `Gap ${ordinal} has no id.`));
2853
- } else if (typeof gap.id === "string") {
2854
- if (seen.has(gap.id)) {
2855
- repeated.add(gap.id);
2856
- }
2857
- seen.add(gap.id);
2858
- gapIds.push(gap.id);
2859
- }
2860
- const hasOwn = !isUnset(gap.choices);
2861
- const hasBank = !isUnwritten(gap.bankId);
2862
- const bankBlank = typeof gap.bankId === "string" && gap.bankId.trim() === "";
2863
- if (bankBlank) {
2864
- issues.push(
2865
- issue(
2866
- "gs_choice_source_required",
2867
- ["gaps", index, "bankId"],
2868
- `Choose a word bank for gap ${name}, or remove the empty field.`
2869
- )
2870
- );
2871
- } else if (hasOwn && hasBank) {
2872
- atGaps.push(
2873
- issue(
2874
- "gs_choice_source_conflict",
2875
- ["gaps"],
2876
- `Gap ${name} has both its own choices and a word bank. Use one or the other.`
2877
- )
2878
- );
2879
- } else if (!hasOwn && !hasBank) {
2880
- atGaps.push(
2881
- issue(
2882
- "gs_choice_source_required",
2883
- ["gaps"],
2884
- `Give gap ${name} a list of choices, or point it at a word bank.`
2885
- )
2886
- );
2887
- }
2888
- if (hasBank && typeof gap.bankId === "string" && !bankIds.has(gap.bankId)) {
2889
- atGaps.push(
2890
- issue(
2891
- "gs_bank_unknown",
2892
- ["gaps"],
2893
- `Gap ${name} uses the word bank "${gap.bankId}", which does not exist.`
2894
- )
2895
- );
2896
- }
2897
- if (hasOwn) {
2898
- issues.push(...checkChoiceList(gap.choices, ["gaps", index, "choices"], `gap ${name}`));
2899
- }
2900
- issues.push(...checkAnswerKey(gap, index, name, banks, bankIds));
2901
- }
2902
- for (const id of repeated) {
2903
- atGaps.push(issue("gs_gap_id_duplicate", ["gaps"], `More than one gap has the id "${id}".`));
2904
- }
2905
- return [...issues, ...atGaps];
2906
- }
2907
- function checkAnswerKey(gap, index, name, banks, bankIds) {
2908
- const key = gap.correctChoiceId;
2909
- if (isUnwritten(key)) {
2910
- return [
2911
- issue(
2912
- "gs_correct_choice_required",
2913
- ["gaps", index, "correctChoiceId"],
2914
- `Mark the correct choice for gap ${name}.`
2915
- )
2916
- ];
2917
- }
2918
- if (typeof key !== "string") {
2919
- return [];
2920
- }
2921
- const choices = resolveChoices(gap, banks, bankIds);
2922
- if (choices === void 0) {
2923
- return [];
2924
- }
2925
- const offered = choices.some((choice) => isRecord(choice) && choice.id === key);
2926
- return offered ? [] : [
2927
- issue(
2928
- "gs_correct_choice_unknown",
2929
- ["gaps"],
2930
- `Gap ${name} is marked correct on "${key}", which is not one of its choices.`
2931
- )
2932
- ];
2933
- }
2934
- function resolveChoices(gap, banks, bankIds) {
2935
- const hasOwn = !isUnset(gap.choices);
2936
- const hasBank = !isUnwritten(gap.bankId);
2937
- if (hasOwn === hasBank) {
2938
- return void 0;
2939
- }
2940
- if (hasOwn) {
2941
- return Array.isArray(gap.choices) ? gap.choices : void 0;
2942
- }
2943
- if (typeof gap.bankId !== "string" || !bankIds.has(gap.bankId) || !Array.isArray(banks)) {
2944
- return void 0;
2945
- }
2946
- const bank = banks.find((entry) => isRecord(entry) && entry.id === gap.bankId);
2947
- return isRecord(bank) && Array.isArray(bank.choices) ? bank.choices : void 0;
2948
- }
2949
- function checkChoiceList(choices, path, owner) {
2950
- const issues = [];
2951
- const list = isUnset(choices) ? [] : choices;
2952
- if (!Array.isArray(list)) {
2953
- return issues;
2954
- }
2955
- if (list.length < MIN_CHOICES) {
2956
- issues.push(
2957
- issue(
2958
- "gs_choices_too_few",
2959
- path,
2960
- `Give ${owner} at least ${MIN_CHOICES} choices to pick from.`
2961
- )
2962
- );
2963
- }
2964
- const seen = /* @__PURE__ */ new Set();
2965
- const repeated = /* @__PURE__ */ new Set();
2966
- for (const [index, choice] of list.entries()) {
2967
- if (!isRecord(choice)) {
2968
- continue;
2969
- }
2970
- if (isMissingId(choice.id)) {
2971
- issues.push(
2972
- issue("gs_choice_id_required", [...path, index, "id"], `Choice ${index + 1} has no id.`)
2973
- );
2974
- } else if (typeof choice.id === "string") {
2975
- if (seen.has(choice.id)) {
2976
- repeated.add(choice.id);
2977
- }
2978
- seen.add(choice.id);
2979
- }
2980
- if (isUnwritten(choice.text)) {
2981
- issues.push(
2982
- issue(
2983
- "gs_choice_text_required",
2984
- [...path, index, "text"],
2985
- `Write the text of choice ${index + 1}.`
2986
- )
2987
- );
2988
- }
2989
- }
2990
- for (const id of repeated) {
2991
- issues.push(issue("gs_choice_id_duplicate", path, `More than one choice has the id "${id}".`));
2992
- }
2993
- return issues;
2994
- }
2995
- function checkPairing2(passage, gaps, gapIds) {
2996
- const issues = [];
2997
- const placeholders = /* @__PURE__ */ new Map();
2998
- for (const match of passage.matchAll(PLACEHOLDER_RE)) {
2999
- const id = match[1];
3000
- placeholders.set(id, (placeholders.get(id) ?? 0) + 1);
3001
- }
3002
- const ids = new Set(gapIds);
3003
- for (const [id, count] of placeholders) {
3004
- if (count > 1) {
3005
- issues.push(
3006
- issue(
3007
- "gs_placeholder_duplicate",
3008
- ["passage"],
3009
- `{{${id}}} appears more than once in the passage. Each gap goes in one place.`
3010
- )
3011
- );
3012
- }
3013
- if (!ids.has(id)) {
3014
- issues.push(
3015
- issue(
3016
- "gs_gap_missing",
3017
- ["passage"],
3018
- `The passage has {{${id}}}, but there is no gap with that id.`
3019
- )
3020
- );
3021
- }
3022
- }
3023
- for (const id of ids) {
3024
- if (!placeholders.has(id)) {
3025
- issues.push(
3026
- issue(
3027
- "gs_placeholder_missing",
3028
- ["passage"],
3029
- `Gap "${id}" is not in the passage. Put {{${id}}} where it goes.`
3030
- )
3031
- );
3032
- }
3033
- }
3034
- if (issues.length === 0 && !pairsOneToOne2(placeholders, gaps)) {
3035
- issues.push(
3036
- issue(
3037
- "gs_gaps_mismatch",
3038
- ["passage"],
3039
- "The gaps and the {{id}} placeholders in the passage do not pair up one to one."
3040
- )
3041
- );
3042
- }
3043
- return issues;
3044
- }
3045
- function pairsOneToOne2(placeholders, gaps) {
3046
- const ids = gaps.filter(isRecord).map((gap) => gap.id);
3047
- if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) {
3048
- return false;
3049
- }
3050
- return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
3051
- }
3052
-
3053
- // src/authoring/multiple-choice.ts
3054
- var MIN_OPTIONS = 2;
3055
- var MAX_OPTIONS = 26;
3056
- var multipleChoiceAuthoring = {
3057
- createDraft: ({ newId }) => ({
3058
- schemaVersion: "1.0",
3059
- type: "multiple-choice",
3060
- id: newId(),
3061
- title: "",
3062
- question: "",
3063
- mode: "single",
3064
- scoringStrategy: "all-or-nothing",
3065
- options: [
3066
- { id: newId(), text: "", isCorrect: false },
3067
- { id: newId(), text: "", isCorrect: false }
3068
- ]
3069
- }),
3070
- checkDraft: checkMultipleChoiceDraft
3071
- };
3072
- function checkMultipleChoiceDraft(draft) {
3073
- const issues = checkIdentity(draft, "multiple-choice");
3074
- if (isUnwritten(draft.question)) {
3075
- issues.push(issue("mc_question_required", ["question"], "Write the question."));
3076
- }
3077
- if (isUnwritten(draft.mode)) {
3078
- issues.push(
3079
- issue("mc_mode_required", ["mode"], "Choose whether learners select one option or several.")
3080
- );
3081
- }
3082
- issues.push(...checkScoringStrategy(draft));
3083
- const options = isUnset(draft.options) ? [] : draft.options;
3084
- if (Array.isArray(options)) {
3085
- if (options.length < MIN_OPTIONS) {
3086
- issues.push(issue("mc_options_too_few", ["options"], "Add at least two options."));
3087
- }
3088
- if (options.length > MAX_OPTIONS) {
3089
- issues.push(
3090
- issue("mc_options_too_many", ["options"], `Use no more than ${MAX_OPTIONS} options.`)
3091
- );
3092
- }
3093
- const seen = /* @__PURE__ */ new Set();
3094
- const repeated = /* @__PURE__ */ new Set();
3095
- let correct = 0;
3096
- for (const [index, option] of options.entries()) {
3097
- if (!isRecord(option)) {
3098
- continue;
3099
- }
3100
- const ordinal = index + 1;
3101
- if (isMissingId(option.id)) {
3102
- issues.push(
3103
- issue("mc_option_id_required", ["options", index, "id"], `Option ${ordinal} has no id.`)
3104
- );
3105
- } else if (typeof option.id === "string") {
3106
- if (seen.has(option.id)) {
3107
- repeated.add(option.id);
3108
- }
3109
- seen.add(option.id);
3110
- }
3111
- if (isUnwritten(option.text)) {
3112
- issues.push(
3113
- issue(
3114
- "mc_option_text_required",
3115
- ["options", index, "text"],
3116
- `Write the text of option ${ordinal}.`
3117
- )
3118
- );
3119
- }
3120
- if (isUnset(option.isCorrect)) {
3121
- issues.push(
3122
- issue(
3123
- "mc_option_correctness_required",
3124
- ["options", index, "isCorrect"],
3125
- `Say whether option ${ordinal} is correct.`
3126
- )
3127
- );
3128
- }
3129
- if (option.isCorrect === true) {
3130
- correct += 1;
3131
- }
3132
- if (isRecord(option.media)) {
3133
- issues.push(...checkOptionMedia(option.media, index, ordinal));
3134
- }
3135
- }
3136
- for (const id of repeated) {
3137
- issues.push(
3138
- issue("mc_option_id_duplicate", ["options"], `More than one option has the id "${id}".`)
3139
- );
3140
- }
3141
- if (correct === 0) {
3142
- issues.push(
3143
- issue(
3144
- "mc_correct_option_required",
3145
- ["options"],
3146
- draft.mode === "multi" ? "Mark at least one option as correct." : "Mark the correct option."
3147
- )
3148
- );
3149
- } else if (draft.mode === "single" && correct > 1) {
3150
- issues.push(
3151
- issue(
3152
- "mc_single_mode_one_correct",
3153
- ["options"],
3154
- "Only one option can be marked correct when learners select one option."
3155
- )
3156
- );
3157
- }
3158
- }
3159
- issues.push(...checkSharedOptional(draft));
3160
- return issues;
3161
- }
3162
- function checkOptionMedia(media, index, ordinal) {
3163
- const path = ["options", index, "media"];
3164
- const kind = media.type;
3165
- if (kind === "video" || kind === "embed") {
3166
- return [
3167
- issue(
3168
- "mc_option_media_kind",
3169
- [...path, "type"],
3170
- `Option ${ordinal} carries ${kind === "embed" ? "an embedded player" : "a video"}, which cannot be an option: its controls swallow the click that selects the answer. Use a picture or a recording.`
3171
- )
3172
- ];
3173
- }
3174
- return checkMedia(media, path, MultipleChoiceOptionMediaSchema);
3175
- }
3176
-
3177
- // src/schemas/read-aloud.ts
3178
- import { z as z8 } from "zod/v4";
3179
-
3180
- // src/scoring/speech/locale.ts
3181
- var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
3182
-
3183
- // src/schemas/read-aloud.ts
3184
- var READ_ALOUD_DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
3185
- var READ_ALOUD_MAX_DIMENSIONS = READ_ALOUD_DIMENSIONS.length;
3186
- var ReadAloudSlowMediaSchema = z8.strictObject({
3187
- type: z8.literal("audio"),
3188
- url: MediaUrlSchema,
3189
- alt: z8.string().min(1).optional()
3190
- });
3191
- var ReferenceTextSchema = z8.string().min(1).refine((text) => text.trim() !== "", {
3192
- error: "The text to read aloud must contain something to read."
3193
- }).meta({ maxLength: READ_ALOUD_MAX_REFERENCE_LENGTH });
3194
- var RecordingBoundsSchema = z8.looseObject({
3195
- maxSeconds: z8.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
3196
- minSeconds: z8.number().min(0).optional(),
3197
- maxTakes: z8.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
3198
- });
3199
- var ReadAloudDimensionWeightSchema = z8.looseObject({
3200
- name: z8.enum(READ_ALOUD_DIMENSIONS),
3201
- weight: z8.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
3202
- });
3203
- var ReadAloudScoringSchema = z8.looseObject({
3204
- dimensions: z8.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
3205
- });
3206
- var ReadAloudDataShape = z8.looseObject({
3207
- schemaVersion: z8.literal("1.0"),
3208
- type: z8.literal("read-aloud"),
3209
- id: z8.string().min(1),
3210
- title: z8.string().min(1),
3211
- instructions: z8.string().optional(),
3212
- referenceText: ReferenceTextSchema,
3213
- locale: z8.string().regex(CANONICAL_LOCALE_RE, {
3214
- error: `The locale must be a BCP 47 tag in canonical form, with a region: "en-US", "es-419", "zh-Hant-TW". Pronunciation is assessed against one locale's speech, and "en" does not say whose.`
3215
- }),
3216
- media: MediaSchema.optional(),
3217
- slowMedia: ReadAloudSlowMediaSchema.optional(),
3218
- recording: RecordingBoundsSchema,
3219
- scoring: ReadAloudScoringSchema,
3220
- passThreshold: z8.number().min(0).max(1).optional(),
3221
- feedback: FeedbackSchema.optional(),
3222
- learningObjectives: z8.array(z8.string()).optional(),
3223
- difficultyLevel: z8.literal([1, 2, 3, 4, 5]).optional(),
3224
- ai: AiPermissionsSchema.optional()
3225
- });
3226
- var ReadAloudDataSchema = ReadAloudDataShape.check(
3227
- checkEvenAfterIssues((ctx) => {
3228
- if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
3229
- return;
3230
- }
3231
- const { data, refused } = parsedFields(ctx);
3232
- const report = (path, input, message) => {
3233
- ctx.issues.push({ code: "custom", input, message, path: [...path] });
3234
- };
3235
- if (!refused.has("referenceText")) {
3236
- const text = data.referenceText;
3237
- const measured = dictationNormalizer(void 0).measure(text);
3238
- if (measured.tooLong) {
3239
- report(
3240
- ["referenceText"],
3241
- text,
3242
- `The text to read aloud is at most ${READ_ALOUD_MAX_REFERENCE_LENGTH} characters, before and after normalisation.`
3243
- );
3244
- }
3245
- if (measured.normalized === "") {
3246
- report(
3247
- ["referenceText"],
3248
- text,
3249
- "The text to read aloud must contain a word: after ignoring punctuation and spacing nothing is left, so no word would be marked and every reading would align as one insertion after another."
3250
- );
3251
- }
3252
- if (containsUnspacedScript(text)) {
3253
- report(
3254
- ["referenceText"],
3255
- text,
3256
- "A read-aloud item needs a script written with spaces between its words. The text is compared word by word, and a run of Han, kana, Thai, Lao, Khmer or Myanmar letters is one word however many it holds."
3257
- );
3258
- }
3259
- }
3260
- const mediaParses = !refused.has("media");
3261
- const slowParses = !refused.has("slowMedia");
3262
- if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
3263
- report(
3264
- ["media", "type"],
3265
- data.media.type,
3266
- "A read-aloud model is read aloud: its recording must be audio."
3267
- );
3268
- }
3269
- if (mediaParses && slowParses && data.slowMedia !== void 0) {
3270
- if (data.media === void 0) {
3271
- report(
3272
- ["media"],
3273
- data.media,
3274
- "A slow model recording accompanies a recording: add `media` first."
3275
- );
3276
- }
3277
- if (data.media?.playback?.maxPlays !== void 0) {
3278
- report(
3279
- ["slowMedia"],
3280
- data.slowMedia,
3281
- "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays`."
3282
- );
3283
- }
3284
- if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
3285
- report(
3286
- ["slowMedia", "url"],
3287
- data.slowMedia.url,
3288
- "The slow model recording must be a different file from the recording."
3289
- );
3290
- }
3291
- }
3292
- if (!refused.has("scoring")) {
3293
- const dimensions = data.scoring.dimensions;
3294
- const seen = /* @__PURE__ */ new Set();
3295
- for (const [index, dimension] of dimensions.entries()) {
3296
- if (seen.has(dimension.name)) {
3297
- report(
3298
- ["scoring", "dimensions", index, "name"],
3299
- dimension.name,
3300
- `"${dimension.name}" is already weighed: a dimension counted twice hides which weight the grade used.`
3301
- );
3302
- }
3303
- seen.add(dimension.name);
3304
- }
3305
- if (dimensions.every((dimension) => dimension.weight === 0)) {
3306
- report(
3307
- ["scoring", "dimensions"],
3308
- dimensions,
3309
- "At least one dimension must carry a weight above 0: with every weight at 0 there is no weighted total, and every take would be unscorable."
3310
- );
3311
- }
3312
- }
3313
- if (!refused.has("recording")) {
3314
- const { minSeconds, maxSeconds } = data.recording;
3315
- if (minSeconds !== void 0 && minSeconds >= maxSeconds) {
3316
- report(
3317
- ["recording", "minSeconds"],
3318
- minSeconds,
3319
- "The shortest take must be shorter than the longest."
3320
- );
3321
- }
3322
- }
3323
- })
3324
- );
3325
-
3326
- // src/authoring/read-aloud.ts
3327
- var readAloudAuthoring = {
3328
- createDraft: ({ newId }) => ({
3329
- schemaVersion: "1.0",
3330
- type: "read-aloud",
3331
- id: newId(),
3332
- title: "",
3333
- referenceText: "",
3334
- locale: "",
3335
- recording: { maxSeconds: 0 },
3336
- scoring: { dimensions: [] }
3337
- }),
3338
- checkDraft: checkReadAloudDraft
3339
- };
3340
- function checkReadAloudDraft(draft) {
3341
- const issues = checkIdentity(draft, "read-aloud");
3342
- issues.push(...checkReferenceText(draft.referenceText));
3343
- issues.push(...checkLocale(draft.locale));
3344
- issues.push(...checkRecording2(draft.recording));
3345
- issues.push(...checkDimensions(draft.scoring));
3346
- issues.push(...checkModelRecording(draft.media));
3347
- issues.push(...checkSlowRecording2(draft));
3348
- issues.push(...checkSharedOptional(draft));
3349
- return issues;
3350
- }
3351
- function checkReferenceText(referenceText) {
3352
- if (isUnwritten(referenceText)) {
3353
- return [
3354
- issue("ra_reference_text_required", ["referenceText"], "Write the text to be read aloud.")
3355
- ];
3356
- }
3357
- if (typeof referenceText !== "string") {
3358
- return [];
3359
- }
3360
- const issues = [];
3361
- const measured = dictationNormalizer(void 0).measure(referenceText);
3362
- if (measured.tooLong) {
3363
- issues.push(
3364
- issue(
3365
- "ra_reference_text_too_long",
3366
- ["referenceText"],
3367
- `The text is longer than ${READ_ALOUD_MAX_REFERENCE_LENGTH} characters, before or after normalisation. A read-aloud item is a sentence or a short paragraph.`
3368
- )
3369
- );
3370
- }
3371
- if (measured.normalized === "") {
3372
- issues.push(
3373
- issue(
3374
- "ra_reference_text_unreadable",
3375
- ["referenceText"],
3376
- "The text has no word to read: once punctuation and spacing are ignored, nothing is left. A reading of it would be marked against no word at all."
3377
- )
3378
- );
3379
- }
3380
- if (containsUnspacedScript(referenceText)) {
3381
- issues.push(
3382
- issue(
3383
- "ra_reference_text_unspaced_script",
3384
- ["referenceText"],
3385
- "The text is written in a script that does not separate its words with spaces. Each word is marked on its own, and a run of Han, kana, Thai, Lao, Khmer or Myanmar letters would be marked as one word."
3386
- )
3387
- );
3388
- }
3389
- return issues;
3390
- }
3391
- function checkLocale(locale) {
3392
- if (isUnwritten(locale)) {
3393
- return [
3394
- issue(
3395
- "ra_locale_required",
3396
- ["locale"],
3397
- 'Choose the language the learner reads in, with its region \u2014 "en-US", not "en".'
3398
- )
3399
- ];
3400
- }
3401
- if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) {
3402
- return [];
3403
- }
3404
- return [
3405
- issue(
3406
- "ra_locale_invalid",
3407
- ["locale"],
3408
- `The language must be a BCP 47 tag in canonical form, with a region: "en-US", "es-419", "zh-Hant-TW". Pronunciation is assessed against one locale's speech.`
3409
- )
3410
- ];
3411
- }
3412
- function checkRecording2(recording) {
3413
- if (isUnset(recording)) {
3414
- return [maxSecondsRequired()];
3415
- }
3416
- if (!isRecord(recording)) {
3417
- return [];
3418
- }
3419
- const issues = [];
3420
- const max = recording.maxSeconds;
3421
- if (isUnset(max) || max === 0) {
3422
- issues.push(maxSecondsRequired());
3423
- } else if (typeof max === "number" && !(Number.isFinite(max) && max > 0 && max <= READ_ALOUD_MAX_SECONDS)) {
3424
- issues.push(
3425
- issue(
3426
- "ra_max_seconds_out_of_range",
3427
- ["recording", "maxSeconds"],
3428
- `A take is at most ${READ_ALOUD_MAX_SECONDS} seconds long.`
3429
- )
3430
- );
3431
- }
3432
- const min = recording.minSeconds;
3433
- if (typeof min === "number" && !(Number.isFinite(min) && min >= 0)) {
3434
- issues.push(minSecondsOutOfRange("The shortest take is a number of seconds, 0 or more."));
3435
- } else if (typeof min === "number" && typeof max === "number" && max > 0 && min >= max) {
3436
- issues.push(minSecondsOutOfRange("The shortest take must be shorter than the longest."));
3437
- }
3438
- const takes = recording.maxTakes;
3439
- if (typeof takes === "number" && !(isWholeNumber(takes) && takes >= 1 && takes <= READ_ALOUD_MAX_TAKES)) {
3440
- issues.push(
3441
- issue(
3442
- "ra_max_takes_out_of_range",
3443
- ["recording", "maxTakes"],
3444
- `How many takes a learner may record is a whole number from 1 to ${READ_ALOUD_MAX_TAKES}.`
3445
- )
3446
- );
3447
- }
3448
- return issues;
3449
- }
3450
- function maxSecondsRequired() {
3451
- return issue(
3452
- "ra_max_seconds_required",
3453
- ["recording", "maxSeconds"],
3454
- "Set how long a take may be, in seconds."
3455
- );
3456
- }
3457
- function minSecondsOutOfRange(message) {
3458
- return issue("ra_min_seconds_out_of_range", ["recording", "minSeconds"], message);
3459
- }
3460
- var DIMENSION_NAMES = READ_ALOUD_DIMENSIONS;
3461
- function checkDimensions(scoring) {
3462
- if (isUnset(scoring)) {
3463
- return [dimensionsRequired()];
3464
- }
3465
- if (!isRecord(scoring)) {
3466
- return [];
3467
- }
3468
- const dimensions = scoring.dimensions;
3469
- if (isUnset(dimensions)) {
3470
- return [dimensionsRequired()];
3471
- }
3472
- if (!Array.isArray(dimensions)) {
3473
- return [];
3474
- }
3475
- if (dimensions.length === 0) {
3476
- return [dimensionsRequired()];
3477
- }
3478
- const issues = [];
3479
- const seen = /* @__PURE__ */ new Set();
3480
- let everyWeightUsable = true;
3481
- let totalWeight = 0;
3482
- for (const [index, dimension] of dimensions.entries()) {
3483
- if (!isRecord(dimension)) {
3484
- everyWeightUsable = false;
3485
- continue;
3486
- }
3487
- const path = ["scoring", "dimensions", index];
3488
- const name = dimension.name;
3489
- if (!DIMENSION_NAMES.includes(name)) {
3490
- issues.push(
3491
- issue(
3492
- "ra_dimension_name_invalid",
3493
- [...path, "name"],
3494
- `Choose what dimension ${index + 1} measures: ${READ_ALOUD_DIMENSIONS.join(", ")}.`
3495
- )
3496
- );
3497
- } else if (seen.has(name)) {
3498
- issues.push(
3499
- issue(
3500
- "ra_dimension_duplicate",
3501
- [...path, "name"],
3502
- `"${String(name)}" is already weighed. Remove this one, or measure something else.`
3503
- )
3504
- );
3505
- } else {
3506
- seen.add(name);
3507
- }
3508
- const weight = dimension.weight;
3509
- if (typeof weight === "number" && Number.isFinite(weight) && weight >= 0 && weight <= READ_ALOUD_MAX_DIMENSION_WEIGHT) {
3510
- totalWeight += weight;
3511
- } else {
3512
- everyWeightUsable = false;
3513
- issues.push(
3514
- issue(
3515
- "ra_dimension_weight_invalid",
3516
- [...path, "weight"],
3517
- `Give dimension ${index + 1} a weight: a number from 0 to ${READ_ALOUD_MAX_DIMENSION_WEIGHT}.`
3518
- )
3519
- );
3520
- }
3521
- }
3522
- if (everyWeightUsable && totalWeight === 0) {
3523
- issues.push(
3524
- issue(
3525
- "ra_dimension_weights_zero",
3526
- ["scoring", "dimensions"],
3527
- "Give at least one dimension a weight above 0. With every weight at 0 there is no weighted total, and every take would be unscorable."
3528
- )
3529
- );
3530
- }
3531
- return issues;
3532
- }
3533
- function dimensionsRequired() {
3534
- return issue(
3535
- "ra_dimensions_required",
3536
- ["scoring", "dimensions"],
3537
- "Choose what the grade measures: at least one of accuracy, fluency, completeness or prosody, with a weight."
3538
- );
3539
- }
3540
- function checkModelRecording(media) {
3541
- if (!isRecord(media)) {
3542
- return [];
3543
- }
3544
- const kind = media.type;
3545
- return isUnwritten(kind) || kind === "audio" ? [] : [
3546
- issue(
3547
- "ra_media_kind",
3548
- ["media", "type"],
3549
- "A read-aloud model is read aloud: its recording must be audio."
3550
- )
3551
- ];
3552
- }
3553
- function checkSlowRecording2(draft) {
3554
- const slow = draft.slowMedia;
3555
- if (!isRecord(slow)) {
3556
- return [];
3557
- }
3558
- const kind = slow.type;
3559
- const wrongKind = !isUnwritten(kind) && kind !== "audio";
3560
- const issues = wrongKind ? [issue("media_invalid", ["slowMedia", "type"], "The slow model recording must be audio.")] : [];
3561
- issues.push(
3562
- ...checkMedia(
3563
- wrongKind ? { ...slow, type: "audio" } : slow,
3564
- ["slowMedia"],
3565
- ReadAloudSlowMediaSchema
3566
- )
3567
- );
3568
- const media = draft.media;
3569
- if (isUnset(media)) {
3570
- issues.push(
3571
- issue(
3572
- "ra_slow_media_without_media",
3573
- ["media"],
3574
- "A slow model recording accompanies a recording. Add the recording first."
3575
- )
3576
- );
3577
- return issues;
3578
- }
3579
- if (!isRecord(media)) {
3580
- return issues;
3581
- }
3582
- if (isRecord(media.playback) && media.playback.maxPlays !== void 0) {
3583
- issues.push(
3584
- issue(
3585
- "ra_slow_media_beside_play_limit",
3586
- ["slowMedia"],
3587
- "A play budget on the recording cannot coexist with a slow recording that has none. Remove the slow recording, or remove the play limit."
3588
- )
3589
- );
3590
- }
3591
- if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) {
3592
- issues.push(
3593
- issue(
3594
- "ra_slow_media_same_recording",
3595
- ["slowMedia", "url"],
3596
- "The slow model recording is the same file as the recording. Point it at the slower reading."
3597
- )
3598
- );
3599
- }
3600
- return issues;
3601
- }
3602
-
3603
- // src/authoring/written-response.ts
3604
- var writtenResponseAuthoring = {
3605
- createDraft: ({ newId }) => ({
3606
- schemaVersion: "1.0",
3607
- type: "written-response",
3608
- id: newId(),
3609
- title: "",
3610
- prompt: "",
3611
- minWords: 0,
3612
- maxWords: 0
3613
- }),
3614
- checkDraft: checkWrittenResponseDraft
3615
- };
3616
- function checkWrittenResponseDraft(draft) {
3617
- const issues = checkIdentity(draft, "written-response");
3618
- if (isUnwritten(draft.prompt)) {
3619
- issues.push(
3620
- issue(
3621
- "wr_prompt_required",
3622
- ["prompt"],
3623
- isUnwritten(draft.promptHtml) ? "Write the prompt." : "Write the prompt as plain text too. The rich-text prompt is shown only where a sanitiser is supplied; the plain text is shown everywhere else."
3624
- )
3625
- );
3626
- }
3627
- const min = draft.minWords;
3628
- const max = draft.maxWords;
3629
- if (isUnset(min)) {
3630
- issues.push(
3631
- issue(
3632
- "wr_min_words_required",
3633
- ["minWords"],
3634
- "Set the minimum number of words. Use 0 for no minimum."
3635
- )
3636
- );
3637
- } else if (!(isWholeNumber(min) && min >= 0)) {
3638
- issues.push(
3639
- issue(
3640
- "wr_min_words_invalid",
3641
- ["minWords"],
3642
- isTooLarge(min) ? "The minimum word count is too large." : "The minimum word count must be a whole number, 0 or more."
3643
- )
3644
- );
3645
- }
3646
- if (isUnset(max) || max === 0) {
3647
- issues.push(issue("wr_max_words_required", ["maxWords"], "Set the maximum number of words."));
3648
- } else if (!(isWholeNumber(max) && max >= 1)) {
3649
- issues.push(
3650
- issue(
3651
- "wr_max_words_invalid",
3652
- ["maxWords"],
3653
- isTooLarge(max) ? "The maximum word count is too large." : "The maximum word count must be a whole number, 1 or more."
3654
- )
3655
- );
3656
- } else if (typeof min === "number" && max < min) {
3657
- issues.push(
3658
- issue(
3659
- "wr_word_bounds_order",
3660
- ["maxWords"],
3661
- "The maximum word count cannot be lower than the minimum."
3662
- )
3663
- );
3664
- }
3665
- const rubric = draft.rubric;
3666
- if (isRecord(rubric)) {
3667
- const criteria = isUnset(rubric.criteria) ? [] : rubric.criteria;
3668
- if (Array.isArray(criteria)) {
3669
- issues.push(...checkCriteria(criteria));
3670
- }
3671
- }
3672
- issues.push(...checkSharedOptional(draft));
3673
- return issues;
3674
- }
3675
- function checkCriteria(criteria) {
3676
- const issues = [];
3677
- if (criteria.length === 0) {
3678
- issues.push(
3679
- issue(
3680
- "wr_rubric_criteria_required",
3681
- ["rubric", "criteria"],
3682
- "Add at least one rubric criterion, or remove the rubric."
3683
- )
3684
- );
3685
- return issues;
3686
- }
3687
- let totalWeight = 0;
3688
- let everyWeightUsable = true;
3689
- for (const [index, criterion] of criteria.entries()) {
3690
- if (!isRecord(criterion)) {
3691
- everyWeightUsable = false;
3692
- continue;
3693
- }
3694
- const ordinal = index + 1;
3695
- const path = ["rubric", "criteria", index];
3696
- if (isUnwritten(criterion.name)) {
3697
- issues.push(
3698
- issue("wr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`)
3699
- );
3700
- }
3701
- const weight = criterion.weight;
3702
- if (isUnset(weight)) {
3703
- everyWeightUsable = false;
3704
- issues.push(
3705
- issue(
3706
- "wr_criterion_weight_required",
3707
- [...path, "weight"],
3708
- `Give rubric criterion ${ordinal} a weight.`
3709
- )
3710
- );
3711
- } else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
3712
- everyWeightUsable = false;
3713
- issues.push(
3714
- issue(
3715
- "wr_criterion_weight_invalid",
3716
- [...path, "weight"],
3717
- "A rubric weight must be a number, 0 or more."
3718
- )
3719
- );
3720
- } else {
3721
- totalWeight += weight;
3722
- }
3723
- }
3724
- if (everyWeightUsable && totalWeight === 0) {
3725
- issues.push(
3726
- issue(
3727
- "wr_rubric_weights_zero",
3728
- ["rubric", "criteria"],
3729
- "Give at least one rubric criterion a weight above 0. With every weight at 0, no weighted total can be computed."
3730
- )
3731
- );
3732
- } else if (totalWeight === Number.POSITIVE_INFINITY) {
3733
- issues.push(
3734
- issue(
3735
- "wr_rubric_weights_too_large",
3736
- ["rubric", "criteria"],
3737
- "The rubric weights add up to more than can be calculated with. Use smaller weights."
3738
- )
3739
- );
3740
- }
3741
- return issues;
3742
- }
3743
-
3744
- // src/schemas/gap-select.ts
3745
- import { z as z9 } from "zod/v4";
3746
- var GapSelectChoiceSchema = z9.looseObject({
3747
- id: z9.string().min(1),
3748
- text: z9.string().min(1)
3749
- });
3750
- var GapSelectBankSchema = z9.looseObject({
3751
- id: z9.string().min(1),
3752
- choices: z9.array(GapSelectChoiceSchema).min(2)
3753
- });
3754
- var GapSelectGapSchema = z9.looseObject({
3755
- id: z9.string().min(1),
3756
- choices: z9.array(GapSelectChoiceSchema).min(2).optional(),
3757
- bankId: z9.string().min(1).optional(),
3758
- correctChoiceId: z9.string().min(1),
3759
- feedback: z9.string().optional()
3760
- });
3761
- function resolveChoices2(gap, banks) {
3762
- if (gap.choices !== void 0) {
3763
- return gap.bankId === void 0 ? gap.choices : void 0;
3764
- }
3765
- if (gap.bankId === void 0) {
3766
- return void 0;
3767
- }
3768
- return banks.find((bank) => bank.id === gap.bankId)?.choices;
3769
- }
3770
- function placeholderCounts(passage) {
3771
- const counts = /* @__PURE__ */ new Map();
3772
- for (const match of passage.matchAll(PLACEHOLDER_RE)) {
3773
- const id = match[1];
3774
- counts.set(id, (counts.get(id) ?? 0) + 1);
3775
- }
3776
- return counts;
3777
- }
3778
- var GapSelectDataSchema = z9.looseObject({
3779
- schemaVersion: z9.literal("1.0"),
3780
- type: z9.literal("gap-select"),
3781
- id: z9.string().min(1),
3782
- title: z9.string().min(1),
3783
- passage: z9.string().min(1),
3784
- passageHtml: z9.string().optional(),
3785
- gaps: z9.array(GapSelectGapSchema).min(1),
3786
- banks: z9.array(GapSelectBankSchema).optional(),
3787
- scoringStrategy: z9.enum(["all-or-nothing", "partial"]),
3788
- presentation: z9.literal("dropdown").optional(),
3789
- shuffleChoices: z9.boolean().optional(),
3790
- media: MediaSchema.optional(),
3791
- feedback: FeedbackSchema.optional(),
3792
- passThreshold: z9.number().min(0).max(1).optional(),
3793
- locale: z9.string().optional(),
3794
- learningObjectives: z9.array(z9.string()).optional(),
3795
- difficultyLevel: z9.literal([1, 2, 3, 4, 5]).optional(),
3796
- ai: AiPermissionsSchema.optional()
3797
- }).refine(
3798
- (data) => {
3799
- const ids = (data.banks ?? []).map((bank) => bank.id);
3800
- return new Set(ids).size === ids.length;
3801
- },
3802
- { error: "Word bank ids must be unique.", path: ["banks"] }
3803
- ).refine(
3804
- (data) => {
3805
- const ids = data.gaps.map((gap) => gap.id);
3806
- return new Set(ids).size === ids.length;
3807
- },
3808
- { error: "Gap ids must be unique.", path: ["gaps"] }
3809
- ).refine(
3810
- (data) => {
3811
- const counts = placeholderCounts(data.passage);
3812
- const ids = data.gaps.map((gap) => gap.id);
3813
- return counts.size === new Set(ids).size && ids.every((id) => counts.get(id) === 1) && [...counts.values()].every((count) => count === 1);
3814
- },
3815
- {
3816
- error: "Every gap must appear exactly once in the passage as {{gap_id}}, and every {{gap_id}} must have a gap.",
3817
- path: ["passage"]
3818
- }
3819
- ).refine(
3820
- (data) => data.gaps.every((gap) => gap.choices === void 0 !== (gap.bankId === void 0)),
3821
- {
3822
- error: "Each gap needs exactly one choice source: its own `choices`, or a `bankId`.",
3823
- path: ["gaps"]
3824
- }
3825
- ).refine(
3826
- (data) => {
3827
- const bankIds = new Set((data.banks ?? []).map((bank) => bank.id));
3828
- return data.gaps.every((gap) => gap.bankId === void 0 || bankIds.has(gap.bankId));
3829
- },
3830
- { error: "A gap references a `bankId` that no word bank defines.", path: ["gaps"] }
3831
- ).refine(
3832
- (data) => {
3833
- const banks = data.banks ?? [];
3834
- return data.gaps.every((gap) => {
3835
- const choices = resolveChoices2(gap, banks);
3836
- if (choices === void 0) {
3837
- return true;
3838
- }
3839
- const ids = choices.map((choice) => choice.id);
3840
- return new Set(ids).size === ids.length && ids.includes(gap.correctChoiceId);
3841
- });
3842
- },
3843
- {
3844
- error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
3845
- path: ["gaps"]
3846
- }
3847
- );
3848
-
3849
- // src/schemas/redacted.ts
3850
- import { z as z11 } from "zod/v4";
3851
-
3852
- // src/schemas/written-response.ts
3853
- import { z as z10 } from "zod/v4";
3854
- var WrittenResponseRubricCriterionSchema = z10.looseObject({
3855
- name: z10.string().min(1),
3856
- description: z10.string().optional(),
3857
- weight: z10.number().min(0)
3858
- });
3859
- var WrittenResponseRubricSchema = z10.looseObject({
3860
- label: z10.string().optional(),
3861
- criteria: z10.array(WrittenResponseRubricCriterionSchema).min(1)
3862
- });
3863
- var RedactedWrittenResponseRubricCriterionSchema = z10.strictObject({
3864
- name: z10.string().min(1),
3865
- description: z10.string().optional(),
3866
- weight: z10.number().min(0)
3867
- });
3868
- var RedactedWrittenResponseRubricSchema = z10.strictObject({
3869
- label: z10.string().optional(),
3870
- criteria: z10.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
3871
- });
3872
- var WrittenResponseDataSchema = z10.looseObject({
3873
- schemaVersion: z10.literal("1.0"),
3874
- type: z10.literal("written-response"),
3875
- id: z10.string().min(1),
3876
- title: z10.string().min(1),
3877
- prompt: z10.string(),
3878
- promptHtml: z10.string().optional(),
3879
- minWords: z10.number().int().min(0),
3880
- maxWords: z10.number().int().min(1),
3881
- rubric: WrittenResponseRubricSchema.optional(),
3882
- languageTarget: z10.string().optional(),
3883
- media: MediaSchema.optional(),
3884
- feedback: FeedbackSchema.optional(),
3885
- passThreshold: z10.number().min(0).max(1).optional(),
3886
- locale: z10.string().optional(),
3887
- learningObjectives: z10.array(z10.string()).optional(),
3888
- difficultyLevel: z10.literal([1, 2, 3, 4, 5]).optional(),
3889
- ai: AiPermissionsSchema.optional()
3890
- }).refine((data) => data.maxWords >= data.minWords, {
3891
- error: "maxWords must be greater than or equal to minWords.",
3892
- path: ["maxWords"]
3893
- });
3894
-
3895
- // src/schemas/redacted.ts
3896
- var redactedBase = {
3897
- /** Marker distinguishing a redacted projection from full activity data. */
3898
- redacted: z11.literal(true),
3899
- /** Slot identity, carried through redaction so the client and the plan agree. */
3900
- slotKey: z11.string().min(1).optional(),
3901
- schemaVersion: z11.literal("1.0"),
3902
- id: z11.string().min(1),
3903
- title: z11.string().min(1),
3904
- media: RedactedMediaSchema.optional(),
3905
- passThreshold: z11.number().min(0).max(1).optional(),
3906
- locale: z11.string().optional(),
3907
- learningObjectives: z11.array(z11.string()).optional(),
3908
- difficultyLevel: z11.literal([1, 2, 3, 4, 5]).optional(),
3909
- ai: z11.strictObject({ explanations: z11.boolean().optional(), hints: z11.boolean().optional() }).optional()
3910
- };
3911
- var RedactedMultipleChoiceOptionMediaSchema = z11.strictObject({
3912
- type: z11.enum(["image", "audio"]),
3913
- url: z11.string().min(1),
3914
- alt: z11.string().min(1).optional(),
3915
- captionsUrl: z11.string().min(1).optional()
3916
- });
3917
- var RedactedMultipleChoiceOptionSchema = z11.strictObject({
3918
- id: z11.string().min(1),
3919
- text: z11.string().min(1),
3920
- media: RedactedMultipleChoiceOptionMediaSchema.optional()
3921
- });
3922
- var RedactedMultipleChoiceDataSchema = z11.strictObject({
3923
- ...redactedBase,
3924
- type: z11.literal("multiple-choice"),
3925
- question: z11.string().min(1),
3926
- questionHtml: z11.string().optional(),
3927
- mode: z11.enum(["single", "multi"]),
3928
- options: z11.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
3929
- shuffle: z11.boolean().optional()
3930
- });
3931
- var RedactedBlankConfigSchema = z11.strictObject({
3932
- id: z11.string().min(1),
3933
- hint: z11.string().optional()
3934
- });
3935
- var RedactedFillInTheBlanksDataSchema = z11.strictObject({
3936
- ...redactedBase,
3937
- type: z11.literal("fill-in-the-blanks"),
3938
- passage: z11.string().min(1),
3939
- passageHtml: z11.string().optional(),
3940
- blanks: z11.array(RedactedBlankConfigSchema).min(1)
3941
- });
3942
- var RedactedGapSelectChoiceSchema = z11.strictObject({
3943
- id: z11.string().min(1),
3944
- text: z11.string().min(1)
3945
- });
3946
- var RedactedGapSelectBankSchema = z11.strictObject({
3947
- id: z11.string().min(1),
3948
- choices: z11.array(RedactedGapSelectChoiceSchema).min(2)
3949
- });
3950
- var RedactedGapSelectGapSchema = z11.strictObject({
3951
- id: z11.string().min(1),
3952
- choices: z11.array(RedactedGapSelectChoiceSchema).min(2).optional(),
3953
- bankId: z11.string().min(1).optional()
3954
- });
3955
- var RedactedGapSelectDataSchema = z11.strictObject({
3956
- ...redactedBase,
3957
- type: z11.literal("gap-select"),
3958
- passage: z11.string().min(1),
3959
- passageHtml: z11.string().optional(),
3960
- gaps: z11.array(RedactedGapSelectGapSchema).min(1),
3961
- banks: z11.array(RedactedGapSelectBankSchema).optional(),
3962
- presentation: z11.literal("dropdown").optional(),
3963
- shuffleChoices: z11.boolean().optional()
3964
- });
3965
- var RedactedWrittenResponseDataSchema = z11.strictObject({
3966
- ...redactedBase,
3967
- type: z11.literal("written-response"),
3968
- prompt: z11.string(),
3969
- promptHtml: z11.string().optional(),
3970
- minWords: z11.number().int().min(0),
3971
- maxWords: z11.number().int().min(1),
3972
- rubric: RedactedWrittenResponseRubricSchema.optional(),
3973
- languageTarget: z11.string().optional()
3974
- });
3975
- var RedactedDictationSlowMediaSchema = z11.strictObject({
3976
- type: z11.literal("audio"),
3977
- url: MediaUrlSchema,
3978
- alt: z11.string().min(1).optional()
3979
- });
3980
- var RedactedDictationDataSchema = z11.strictObject({
3981
- ...redactedBase,
3982
- type: z11.literal("dictation"),
3983
- slowMedia: RedactedDictationSlowMediaSchema.optional(),
3984
- hints: z11.strictObject({ mode: z11.literal("progressive-words") }).optional()
3985
- }).check((ctx) => {
3986
- const data = ctx.value;
3987
- for (const captions of captionFieldsOf(data.media)) {
3988
- ctx.issues.push({
3989
- code: "custom",
3990
- input: data.media?.[captions],
3991
- message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
3992
- path: ["media", captions]
3993
- });
3994
- }
3995
- if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
3996
- ctx.issues.push({
3997
- code: "custom",
3998
- input: data.slowMedia,
3999
- message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
4000
- path: ["slowMedia"]
4001
- });
4002
- }
4003
- if (data.slowMedia !== void 0 && data.media === void 0) {
4004
- ctx.issues.push({
4005
- code: "custom",
4006
- input: data.slowMedia,
4007
- message: "A slow recording accompanies a recording: this payload has no `media`.",
4008
- path: ["media"]
4009
- });
4010
- }
4011
- });
4012
- var RedactedReadAloudDataSchema = z11.strictObject({
4013
- ...redactedBase,
4014
- type: z11.literal("read-aloud"),
4015
- locale: z11.string().regex(CANONICAL_LOCALE_RE),
4016
- instructions: z11.string().optional(),
4017
- referenceText: z11.string().min(1),
4018
- slowMedia: ReadAloudSlowMediaSchema.optional(),
4019
- recording: z11.strictObject({
4020
- maxSeconds: z11.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
4021
- minSeconds: z11.number().min(0).optional(),
4022
- maxTakes: z11.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
4023
- }),
4024
- scoring: z11.strictObject({
4025
- dimensions: z11.array(
4026
- z11.strictObject({
4027
- name: z11.enum(READ_ALOUD_DIMENSIONS),
4028
- weight: z11.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
4029
- })
4030
- ).min(1).max(READ_ALOUD_DIMENSIONS.length)
4031
- })
4032
- }).check((ctx) => {
4033
- const data = ctx.value;
4034
- if (data.media !== void 0 && data.media.type !== "audio") {
4035
- ctx.issues.push({
4036
- code: "custom",
4037
- input: data.media.type,
4038
- message: "A read-aloud model recording must be audio, so this payload is not renderable.",
4039
- path: ["media", "type"]
4040
- });
4041
- }
4042
- if (data.slowMedia !== void 0 && data.media === void 0) {
4043
- ctx.issues.push({
4044
- code: "custom",
4045
- input: data.slowMedia,
4046
- message: "A slow model recording accompanies a recording: this payload has no `media`.",
4047
- path: ["media"]
4048
- });
4049
- }
4050
- if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
4051
- ctx.issues.push({
4052
- code: "custom",
4053
- input: data.slowMedia,
4054
- message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
4055
- path: ["slowMedia"]
4056
- });
4057
- }
4058
- if (data.slowMedia !== void 0 && data.slowMedia.url === data.media?.url) {
4059
- ctx.issues.push({
4060
- code: "custom",
4061
- input: data.slowMedia.url,
4062
- message: "The slow model recording must be a different file from the recording.",
4063
- path: ["slowMedia", "url"]
4064
- });
4065
- }
4066
- });
4067
-
4068
- // src/scoring/activity-scorers/dictation.ts
4069
- function scoreDictation(data, response) {
4070
- const alignment = alignDictation(data, typeof response?.text === "string" ? response.text : "");
4071
- const details = [];
4072
- for (const word of alignment.words) {
4073
- if (word.status === "extra") {
4074
- continue;
4075
- }
4076
- details.push({
4077
- itemId: word.itemId,
4078
- outcome: word.status === "correct" ? "correct" : word.status === "incorrect" ? "incorrect" : "incorrect-omission",
4079
- learnerResponse: word.attempt,
4080
- correctResponse: word.reference,
4081
- score: word.similarity,
4082
- weight: 1
4083
- });
4084
- }
4085
- return { score: alignment.similarity, maxScore: 1, feedback: null, details };
4086
- }
4087
-
4088
- // src/scoring/strategies/all-or-nothing.ts
4089
- function allOrNothingStrategy(correctItems) {
4090
- return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
4091
- }
4092
-
4093
- // src/scoring/strategies/partial.ts
4094
- function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
4095
- const reward = correctSelected / totalCorrect;
4096
- const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
4097
- return Math.max(0, reward - penalty);
4098
- }
4099
- function partialBlankStrategy(correctBlanks, totalBlanks) {
4100
- return correctBlanks / totalBlanks;
4101
- }
4102
-
4103
- // src/scoring/activity-scorers/fill-in-the-blanks.ts
4104
- function policyFor(blank) {
4105
- return {
4106
- ...blank.caseSensitive !== void 0 ? { caseSensitive: blank.caseSensitive } : {},
4107
- ...blank.trimWhitespace !== void 0 ? { trim: blank.trimWhitespace } : {},
4108
- ...blank.match
4109
- };
4110
- }
4111
- function scoreFillInTheBlanks(data, response) {
4112
- const details = [];
4113
- const perBlankCorrect = [];
4114
- for (const blank of data.blanks) {
4115
- const rawInput = response.answers[blank.id];
4116
- const input = typeof rawInput === "string" ? rawInput : "";
4117
- const matched = matchText(input, blank.acceptedAnswers, policyFor(blank)).matched;
4118
- perBlankCorrect.push(matched);
4119
- details.push({
4120
- itemId: blank.id,
4121
- outcome: matched ? "correct" : "incorrect",
4122
- learnerResponse: [input],
4123
- correctResponse: [...blank.acceptedAnswers],
4124
- weight: 1
4125
- });
4126
- }
4127
- const correctBlanks = perBlankCorrect.filter(Boolean).length;
4128
- const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
4129
- return { score: scoreValue, maxScore: 1, feedback: null, details };
4130
- }
4131
-
4132
- // src/scoring/activity-scorers/gap-select.ts
4133
- function choicesFor(data, gap) {
4134
- if (gap.choices !== void 0) {
4135
- return gap.choices;
4136
- }
4137
- return data.banks?.find((bank) => bank.id === gap.bankId)?.choices ?? [];
4138
- }
4139
- function scoreGapSelect(data, response) {
4140
- const details = [];
4141
- const perGapCorrect = [];
4142
- for (const gap of data.gaps) {
4143
- const raw = response.selections[gap.id];
4144
- const selected = typeof raw === "string" ? raw : "";
4145
- const choices = choicesFor(data, gap);
4146
- const offered = choices.some((choice) => choice.id === selected);
4147
- const answered = selected !== "" && offered;
4148
- const correct = answered && selected === gap.correctChoiceId;
4149
- perGapCorrect.push(correct);
4150
- details.push({
4151
- itemId: gap.id,
4152
- outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
4153
- learnerResponse: [selected],
4154
- correctResponse: [gap.correctChoiceId],
4155
- weight: 1
4156
- });
4157
- }
4158
- const correctGaps = perGapCorrect.filter(Boolean).length;
4159
- const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perGapCorrect) : partialBlankStrategy(correctGaps, data.gaps.length);
4160
- return { score: scoreValue, maxScore: 1, feedback: null, details };
4161
- }
4162
-
4163
- // src/scoring/activity-scorers/multiple-choice.ts
4164
- function scoreMultipleChoice(data, response) {
4165
- const optionById = new Map(data.options.map((option) => [option.id, option]));
4166
- const selected = new Set(response.selectedOptionIds);
4167
- const totalCorrect = data.options.filter((option) => option.isCorrect).length;
4168
- const totalIncorrect = data.options.length - totalCorrect;
4169
- let scoreValue;
4170
- if (data.scoringStrategy === "all-or-nothing") {
4171
- if (data.mode === "single") {
4172
- scoreValue = response.selectedOptionIds.length === 1 && optionById.get(response.selectedOptionIds[0])?.isCorrect === true ? 1 : 0;
4173
- } else {
4174
- const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
4175
- const allCorrectSelected = correctIds.every((id) => selected.has(id));
4176
- scoreValue = selected.size === correctIds.length && allCorrectSelected ? 1 : 0;
4177
- }
4178
- } else {
4179
- let correctSelected = 0;
4180
- let incorrectSelected = 0;
4181
- for (const id of selected) {
4182
- const option = optionById.get(id);
4183
- if (option?.isCorrect) {
4184
- correctSelected += 1;
4185
- } else {
4186
- incorrectSelected += 1;
4187
- }
4188
- }
4189
- scoreValue = partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect);
4190
- }
4191
- const details = data.options.map((option) => {
4192
- const wasSelected = selected.has(option.id);
4193
- const outcome = wasSelected ? option.isCorrect ? "correct" : "incorrect" : option.isCorrect ? "incorrect-omission" : "correct-omission";
4194
- return {
4195
- itemId: option.id,
4196
- outcome,
4197
- learnerResponse: [wasSelected ? "selected" : "not-selected"],
4198
- correctResponse: [option.isCorrect ? "selected" : "not-selected"],
4199
- weight: 1
4200
- };
4201
- });
4202
- return { score: scoreValue, maxScore: 1, feedback: null, details };
4203
- }
4204
-
4205
- // src/registry/builtins.ts
4206
- var MEDIA_FIELD_POLICY = {
4207
- type: "public",
4208
- url: "public",
4209
- alt: "public",
4210
- captionsUrl: "public",
4211
- poster: "public",
4212
- // Applies to every element of the array.
4213
- tracks: {
4214
- kind: "public",
4215
- src: "public",
4216
- srclang: "public",
4217
- label: "public",
4218
- default: "public"
4219
- },
4220
- playback: {
4221
- controls: "public",
4222
- maxPlays: "public",
4223
- seek: "public",
4224
- rate: "public",
4225
- nativeControlHints: "public"
4226
- }
4227
- };
4228
- var SHARED_PUBLIC_FIELDS = {
4229
- schemaVersion: "public",
4230
- type: "public",
4231
- id: "public",
4232
- // Assembly metadata, not content: it names the slot this item occupies in a
4233
- // paper. It has to survive redaction, or the exam client derives positional
4234
- // slot ids while the server's stored plan holds keyed ones, and the
4235
- // responses cannot be matched back to the attempt.
4236
- slotKey: "public",
4237
- title: "public",
4238
- media: MEDIA_FIELD_POLICY,
4239
- passThreshold: "public",
4240
- locale: "public",
4241
- learningObjectives: "public",
4242
- difficultyLevel: "public",
4243
- // What an author allows AI to do for the item. Public, field by field: a
4244
- // review that explains an answer must still honour an author who said no,
4245
- // and an unclassified key parked inside it must not ride along.
4246
- ai: { explanations: "public", hints: "public" }
4247
- };
4248
- var FEEDBACK_FIELD_POLICY = {
4249
- correct: "answer-key",
4250
- incorrect: "answer-key"
4251
- };
4252
- var TEXT_MATCH_FIELD_POLICY = {
4253
- caseSensitive: "answer-key",
4254
- trim: "answer-key",
4255
- normalize: "answer-key",
4256
- foldDiacritics: "answer-key",
4257
- collapseInnerWhitespace: "answer-key",
4258
- ignorePunctuation: "answer-key",
4259
- levenshtein: "answer-key",
4260
- locale: "answer-key"
4261
- };
4262
- var MULTIPLE_CHOICE_FIELD_POLICY = {
4263
- ...SHARED_PUBLIC_FIELDS,
4264
- question: "public",
4265
- questionHtml: "public",
4266
- mode: "public",
4267
- shuffle: "public",
4268
- scoringStrategy: "answer-key",
4269
- feedback: FEEDBACK_FIELD_POLICY,
4270
- options: {
4271
- id: "public",
4272
- text: "public",
4273
- isCorrect: "answer-key",
4274
- feedback: "answer-key",
4275
- // Public, and classified key by key rather than as one leaf — the lesson
4276
- // `media`, `rubric` and `feedback` each taught: a scalar classification
4277
- // assigns the author's object by reference without recursing, so an
4278
- // unclassified key parked inside it survives redact() AND assertRedacted().
4279
- // An option's picture or recording IS the thing the learner picks, so
4280
- // stripping it would ship a row of blank options.
4281
- media: {
4282
- type: "public",
4283
- url: "public",
4284
- alt: "public",
4285
- captionsUrl: "public"
4286
- }
4287
- }
4288
- };
4289
- var GAP_SELECT_FIELD_POLICY = {
4290
- ...SHARED_PUBLIC_FIELDS,
4291
- passage: "public",
4292
- passageHtml: "public",
4293
- presentation: "public",
4294
- shuffleChoices: "public",
4295
- scoringStrategy: "answer-key",
4296
- feedback: FEEDBACK_FIELD_POLICY,
4297
- banks: {
4298
- id: "public",
4299
- choices: { id: "public", text: "public" }
4300
- },
4301
- gaps: {
4302
- id: "public",
4303
- bankId: "public",
4304
- choices: { id: "public", text: "public" },
4305
- correctChoiceId: "answer-key",
4306
- feedback: "answer-key"
4307
- }
4308
- };
4309
- var FILL_IN_THE_BLANKS_FIELD_POLICY = {
4310
- ...SHARED_PUBLIC_FIELDS,
4311
- passage: "public",
4312
- passageHtml: "public",
4313
- scoringStrategy: "answer-key",
4314
- feedback: FEEDBACK_FIELD_POLICY,
4315
- blanks: {
4316
- id: "public",
4317
- hint: "public",
4318
- acceptedAnswers: "answer-key",
4319
- caseSensitive: "answer-key",
4320
- trimWhitespace: "answer-key",
4321
- match: TEXT_MATCH_FIELD_POLICY,
4322
- feedback: "answer-key"
4323
- }
4324
- };
4325
- var DICTATION_FIELD_POLICY = {
4326
- ...SHARED_PUBLIC_FIELDS,
4327
- transcript: "answer-key",
4328
- acceptedTranscripts: "answer-key",
4329
- slowMedia: { type: "public", url: "public", alt: "public" },
4330
- hints: { mode: "public" },
4331
- tolerance: "answer-key",
4332
- feedback: FEEDBACK_FIELD_POLICY
4333
- };
4334
- var RUBRIC_FIELD_POLICY = {
4335
- label: "public",
4336
- // Applies to every element of the array.
4337
- criteria: {
4338
- name: "public",
4339
- description: "public",
4340
- weight: "public"
4341
- }
4342
- };
4343
- var WRITTEN_RESPONSE_FIELD_POLICY = {
4344
- ...SHARED_PUBLIC_FIELDS,
4345
- prompt: "public",
4346
- promptHtml: "public",
4347
- minWords: "public",
4348
- maxWords: "public",
4349
- languageTarget: "public",
4350
- feedback: FEEDBACK_FIELD_POLICY,
4351
- // A rubric is a LEARNER affordance, not a grader secret: it tells the
4352
- // learner what they are being graded on, which is pedagogically the point
4353
- // of publishing one. (Classifying it author-only broke real deployments
4354
- // that render a rubric panel during the attempt.) A deployment that wants
4355
- // it hidden can tighten this per call via `redact(data, { policy })`.
4356
- // Classified field by field so that stays true of the rubric's DOCUMENTED
4357
- // fields only — see {@link RUBRIC_FIELD_POLICY}.
4358
- rubric: RUBRIC_FIELD_POLICY
4359
- };
4360
- var multipleChoiceType = defineActivityType({
4361
- type: "multiple-choice",
4362
- // zod4 optional outputs are `T | undefined`; the hand-written wire types use
4363
- // exact optionals. Structurally identical at runtime — cast is type-level only.
4364
- schema: MultipleChoiceDataSchema,
4365
- scoring: { kind: "sync", score: scoreMultipleChoice },
4366
- isAnswered: (response) => (response?.selectedOptionIds.length ?? 0) > 0,
4367
- fieldPolicy: MULTIPLE_CHOICE_FIELD_POLICY,
4368
- redactedSchema: RedactedMultipleChoiceDataSchema,
4369
- interop: {
4370
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4371
- xapiInteractionType: "choice",
4372
- correctResponsesPattern: (data) => [
4373
- data.options.filter((option) => option.isCorrect).map((option) => option.id).join("[,]")
4374
- ]
4375
- },
4376
- interactions: ["option-selected", "option-deselected", "submitted"],
4377
- authoring: multipleChoiceAuthoring
4378
- });
4379
- var fillInTheBlanksType = defineActivityType({
4380
- type: "fill-in-the-blanks",
4381
- schema: FillInTheBlanksDataSchema,
4382
- scoring: { kind: "sync", score: scoreFillInTheBlanks },
4383
- isAnswered: (response) => Object.values(response?.answers ?? {}).some((answer) => answer.trim().length > 0),
4384
- fieldPolicy: FILL_IN_THE_BLANKS_FIELD_POLICY,
4385
- redactedSchema: RedactedFillInTheBlanksDataSchema,
4386
- interop: {
4387
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4388
- xapiInteractionType: "fill-in",
4389
- // xAPI fill-in pattern: blank answers joined with "[,]". Only the first
4390
- // accepted answer per blank is emitted (full alternates would explode
4391
- // combinatorially); the complete key lives in the activity data.
4392
- correctResponsesPattern: (data) => [
4393
- data.blanks.map((blank) => blank.acceptedAnswers[0] ?? "").join("[,]")
4394
- ]
4395
- },
4396
- interactions: ["blank-filled", "hint-requested", "submitted"],
4397
- authoring: fillInTheBlanksAuthoring
4398
- });
4399
- var writtenResponseType = defineActivityType({
4400
- type: "written-response",
4401
- schema: WrittenResponseDataSchema,
4402
- scoring: {
4403
- kind: "deferred",
4404
- reason: "requires_async_grading",
4405
- partial: (data, response) => {
4406
- const wordCount = countWords(response?.text ?? "");
4407
- return {
4408
- withinWordBounds: response !== void 0 && wordCount >= data.minWords && wordCount <= data.maxWords,
4409
- wordCount
4410
- };
4411
- }
4412
- },
4413
- isAnswered: (response) => (response?.text.trim().length ?? 0) > 0,
4414
- fieldPolicy: WRITTEN_RESPONSE_FIELD_POLICY,
4415
- redactedSchema: RedactedWrittenResponseDataSchema,
4416
- interop: {
4417
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4418
- xapiInteractionType: "long-fill-in",
4419
- correctResponsesPattern: () => []
4420
- },
4421
- interactions: ["text-changed", "submitted"],
4422
- authoring: writtenResponseAuthoring
4423
- });
4424
- var gapSelectType = defineActivityType({
4425
- type: "gap-select",
4426
- schema: GapSelectDataSchema,
4427
- scoring: { kind: "sync", score: scoreGapSelect },
4428
- isAnswered: (response) => Object.values(response?.selections ?? {}).some((choiceId) => choiceId !== ""),
4429
- fieldPolicy: GAP_SELECT_FIELD_POLICY,
4430
- redactedSchema: RedactedGapSelectDataSchema,
4431
- interop: {
4432
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4433
- // `matching`, not `choice` or `fill-in`. The learner pairs a set of sources
4434
- // (the gaps) with a set of targets (the choices), which is exactly what the
4435
- // xAPI matching interaction describes, and it is the only built-in type
4436
- // whose pattern can name WHICH gap took which answer. `fill-in` — what the
4437
- // Fill-in-the-Blanks descriptor uses — would flatten the gaps into an
4438
- // ordered list of strings and lose that.
4439
- xapiInteractionType: "matching",
4440
- correctResponsesPattern: (data) => [
4441
- data.gaps.map((gap) => `${gap.id}[.]${gap.correctChoiceId}`).join("[,]")
4442
- ]
4443
- },
4444
- interactions: ["gap-selected", "submitted"],
4445
- authoring: gapSelectAuthoring
4446
- });
4447
- var dictationType = defineActivityType({
4448
- type: "dictation",
4449
- schema: DictationDataSchema,
4450
- scoring: { kind: "sync", score: scoreDictation },
4451
- // Whitespace-only or punctuation-only text is no answer. Without the
4452
- // tolerance, which this signature cannot see: the one gap is an attempt made
4453
- // ONLY of a symbol an equivalence names, which reads as unanswered here and
4454
- // is scored as the rewritten word — documented in the authoring guide. The
4455
- // reverse cannot happen: a rule inserts words, never nothing, and the text is
4456
- // cut where the scorer cuts, so text past the cap never counts as an answer.
4457
- isAnswered: (response) => normalizeDictationText(
4458
- truncateCodePoints(
4459
- typeof response?.text === "string" ? response.text : "",
4460
- DICTATION_MAX_TEXT_LENGTH
4461
- )
4462
- ) !== "",
4463
- fieldPolicy: DICTATION_FIELD_POLICY,
4464
- redactedSchema: RedactedDictationDataSchema,
4465
- interop: {
4466
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4467
- // `fill-in`, as for fill-in-the-blanks: a typed answer with a key pattern.
4468
- // `long-fill-in` is what written-response uses for free text nothing scores.
4469
- xapiInteractionType: "fill-in",
4470
- correctResponsesPattern: (data) => [data.transcript, ...data.acceptedTranscripts ?? []]
4471
- },
4472
- interactions: ["text-changed", "hint-requested", "submitted"],
4473
- authoring: dictationAuthoring
4474
- });
4475
- var READ_ALOUD_FIELD_POLICY = {
4476
- ...SHARED_PUBLIC_FIELDS,
4477
- instructions: "public",
4478
- referenceText: "public",
4479
- recording: { maxSeconds: "public", minSeconds: "public", maxTakes: "public" },
4480
- slowMedia: { type: "public", url: "public", alt: "public" },
4481
- scoring: { dimensions: { name: "public", weight: "public" } },
4482
- feedback: FEEDBACK_FIELD_POLICY
4483
- };
4484
- function hasRecording(response) {
4485
- const recording = response?.recording;
4486
- return typeof recording === "object" && recording !== null && typeof recording.key === "string" && recording.key !== "";
4487
- }
4488
- var readAloudType = defineActivityType({
4489
- type: "read-aloud",
4490
- schema: ReadAloudDataSchema,
4491
- scoring: {
4492
- kind: "deferred",
4493
- reason: "requires_async_grading",
4494
- partial: (_data, response) => ({ hasRecording: hasRecording(response) })
4495
- },
4496
- isAnswered: (response) => hasRecording(response),
4497
- fieldPolicy: READ_ALOUD_FIELD_POLICY,
4498
- redactedSchema: RedactedReadAloudDataSchema,
4499
- interop: {
4500
- xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
4501
- // `other`, not `performance`: xAPI's performance interaction describes a set
4502
- // of steps with their own responses, and a reading has one. There is no
4503
- // pattern to publish either — the "correct response" is a pronunciation, not
4504
- // a string a statement can carry.
4505
- xapiInteractionType: "other",
4506
- correctResponsesPattern: () => []
4507
- },
4508
- interactions: ["recording-started", "recording-stopped", "recording-uploaded", "submitted"],
4509
- authoring: readAloudAuthoring
4510
- });
4511
- for (const descriptor of [
4512
- multipleChoiceType,
4513
- fillInTheBlanksType,
4514
- writtenResponseType,
4515
- gapSelectType,
4516
- dictationType,
4517
- readAloudType
4518
- ]) {
4519
- ownSchemas(
4520
- descriptor.schema,
4521
- ...descriptor.redactedSchema !== void 0 ? [descriptor.redactedSchema] : []
4522
- );
4523
- }
4524
- registerActivityType(multipleChoiceType);
4525
- registerActivityType(fillInTheBlanksType);
4526
- registerActivityType(writtenResponseType);
4527
- registerActivityType(gapSelectType);
4528
- registerActivityType(dictationType);
4529
- registerActivityType(readAloudType);
4530
-
4531
- export {
4532
- maxUnits,
4533
- MediaSchema,
4534
- RedactedMediaSchema,
4535
- PLACEHOLDER_RE,
4536
- FillInTheBlanksDataSchema,
4537
- withValidationScope,
4538
- DICTATION_MAX_TRANSCRIPT_LENGTH,
4539
- DICTATION_MAX_TEXT_LENGTH,
4540
- DICTATION_MAX_EQUIVALENCE_LENGTH,
4541
- DICTATION_MAX_EQUIVALENCES,
4542
- DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
4543
- codePointLength,
4544
- normalizeDictationText,
4545
- dictationReferenceWords,
4546
- alignDictation,
4547
- STEP,
4548
- alignSequences,
4549
- ActivitySchemaError,
4550
- UnknownActivityTypeError,
4551
- RedactedScoringError,
4552
- DeferredScoringError,
4553
- DictationDataSchema,
4554
- groupCaptionsField,
4555
- GROUP_CAPTIONS_REVEAL_DICTATION,
4556
- DRAFT_ISSUE_SEVERITY,
4557
- issue,
4558
- isRecord,
4559
- refusesEmpty,
4560
- isUnset,
4561
- isUnwritten,
4562
- isMissingId,
4563
- checkMedia,
4564
- covers,
4565
- pathKey,
4566
- coveredPathsOf,
4567
- MultipleChoiceOptionMediaSchema,
4568
- MultipleChoiceDataSchema,
4569
- READ_ALOUD_MAX_REFERENCE_LENGTH,
4570
- READ_ALOUD_MAX_SECONDS,
4571
- READ_ALOUD_MAX_TAKES,
4572
- READ_ALOUD_MAX_DIMENSION_WEIGHT,
4573
- SPEECH_ASSESSMENT_MAX_WORDS,
4574
- SPEECH_ASSESSMENT_MAX_TEXT_LENGTH,
4575
- CANONICAL_LOCALE_RE,
4576
- ReadAloudDataSchema,
4577
- countWords,
4578
- GapSelectDataSchema,
4579
- isOwnSchema,
4580
- readSchema,
4581
- standardJsonSchemaOf,
4582
- WrittenResponseDataSchema,
4583
- levenshteinDistance,
4584
- matchText,
4585
- defineActivityType,
4586
- registerActivityType,
4587
- getActivityTypeDescriptor,
4588
- registeredActivityTypes,
4589
- MEDIA_FIELD_POLICY,
4590
- multipleChoiceType,
4591
- fillInTheBlanksType,
4592
- writtenResponseType,
4593
- gapSelectType,
4594
- dictationType,
4595
- readAloudType
4596
- };
4597
- //# sourceMappingURL=chunk-FBKTION6.js.map