@intellectif/lk-core 0.21.0 → 1.0.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 (66) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -5
  3. package/dist/{activity-xhGV-Aeh.d.cts → activity-DgpBJeBc.d.cts} +137 -22
  4. package/dist/{activity-xhGV-Aeh.d.ts → activity-DgpBJeBc.d.ts} +137 -22
  5. package/dist/ai-check.cjs +104 -16
  6. package/dist/ai-check.cjs.map +1 -1
  7. package/dist/ai-check.d.cts +9 -5
  8. package/dist/ai-check.d.ts +9 -5
  9. package/dist/ai-check.js +103 -15
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{chunk-2CV6NU32.cjs → chunk-2JT2WUQ2.cjs} +77 -39
  12. package/dist/chunk-2JT2WUQ2.cjs.map +1 -0
  13. package/dist/{chunk-D4TOPSHV.cjs → chunk-DSDXL3JO.cjs} +1216 -1105
  14. package/dist/chunk-DSDXL3JO.cjs.map +1 -0
  15. package/dist/{chunk-D3GP5MET.js → chunk-FBKTION6.js} +1093 -982
  16. package/dist/chunk-FBKTION6.js.map +1 -0
  17. package/dist/{chunk-Q7MV3IXJ.js → chunk-IARLJ5MW.js} +15 -16
  18. package/dist/chunk-IARLJ5MW.js.map +1 -0
  19. package/dist/{chunk-IHOEP4EB.js → chunk-RCZCP5OE.js} +311 -25
  20. package/dist/chunk-RCZCP5OE.js.map +1 -0
  21. package/dist/{chunk-X5DAXBJI.js → chunk-SJVRUPJT.js} +2 -2
  22. package/dist/{chunk-EOFWSGLU.cjs → chunk-TJJFKKH2.cjs} +4 -4
  23. package/dist/{chunk-EOFWSGLU.cjs.map → chunk-TJJFKKH2.cjs.map} +1 -1
  24. package/dist/{chunk-WQVZPIN7.cjs → chunk-VSH7ECQL.cjs} +44 -45
  25. package/dist/chunk-VSH7ECQL.cjs.map +1 -0
  26. package/dist/{chunk-NRTP7WCW.cjs → chunk-W5KB3DNT.cjs} +315 -29
  27. package/dist/chunk-W5KB3DNT.cjs.map +1 -0
  28. package/dist/{chunk-Z7QQZ53L.js → chunk-XS6U2ARZ.js} +70 -32
  29. package/dist/chunk-XS6U2ARZ.js.map +1 -0
  30. package/dist/index-BVNeSCWC.d.ts +548 -0
  31. package/dist/index-BlOLlwOi.d.cts +548 -0
  32. package/dist/index.cjs +129 -198
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +175 -18
  35. package/dist/index.d.ts +175 -18
  36. package/dist/index.js +34 -103
  37. package/dist/index.js.map +1 -1
  38. package/dist/schemas.cjs +3 -94
  39. package/dist/schemas.cjs.map +1 -1
  40. package/dist/schemas.d.cts +2 -3
  41. package/dist/schemas.d.ts +2 -3
  42. package/dist/schemas.js +6 -97
  43. package/dist/scoring.cjs +3 -3
  44. package/dist/scoring.d.cts +2 -2
  45. package/dist/scoring.d.ts +2 -2
  46. package/dist/scoring.js +2 -2
  47. package/dist/xapi.cjs +3 -3
  48. package/dist/xapi.d.cts +1 -1
  49. package/dist/xapi.d.ts +1 -1
  50. package/dist/xapi.js +2 -2
  51. package/package.json +4 -4
  52. package/vectors/README.md +32 -2
  53. package/vectors/scoring.json +1 -498
  54. package/vectors/validation.json +6742 -0
  55. package/vectors/validation.mjs +195 -0
  56. package/dist/chunk-2CV6NU32.cjs.map +0 -1
  57. package/dist/chunk-D3GP5MET.js.map +0 -1
  58. package/dist/chunk-D4TOPSHV.cjs.map +0 -1
  59. package/dist/chunk-IHOEP4EB.js.map +0 -1
  60. package/dist/chunk-NRTP7WCW.cjs.map +0 -1
  61. package/dist/chunk-Q7MV3IXJ.js.map +0 -1
  62. package/dist/chunk-WQVZPIN7.cjs.map +0 -1
  63. package/dist/chunk-Z7QQZ53L.js.map +0 -1
  64. package/dist/index-BuD8Hc8o.d.cts +0 -2303
  65. package/dist/index-DnJ_PI4d.d.ts +0 -2303
  66. /package/dist/{chunk-X5DAXBJI.js.map → chunk-SJVRUPJT.js.map} +0 -0
@@ -1,19 +1,22 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/schemas/ai.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/schemas/media.ts
2
2
  var _v4 = require('zod/v4');
3
- var AiPermissionsSchema = _v4.z.looseObject({
4
- explanations: _v4.z.boolean().optional(),
5
- hints: _v4.z.boolean().optional()
6
- });
7
3
 
8
- // src/schemas/feedback.ts
9
-
10
- var FeedbackSchema = _v4.z.looseObject({
11
- correct: _v4.z.string().min(1).optional(),
12
- incorrect: _v4.z.string().min(1).optional()
13
- });
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
+ }
14
18
 
15
19
  // src/schemas/media.ts
16
-
17
20
  var MediaUrlSchema = _v4.z.union([
18
21
  _v4.z.url().refine((value) => /^(https?|data|blob):/i.test(value), {
19
22
  error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
@@ -52,7 +55,9 @@ var MediaTrackSchema = _v4.z.strictObject({
52
55
  srclang: _v4.z.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
53
56
  error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
54
57
  }),
55
- label: _v4.z.string().max(60).refine((label) => label.trim().length > 0, { error: "A track label must not be empty." }),
58
+ label: maxUnits(_v4.z.string(), 60).refine((label) => label.trim().length > 0, {
59
+ error: "A track label must not be empty."
60
+ }),
56
61
  default: _v4.z.boolean().optional()
57
62
  });
58
63
  var MAX_TRACKS = 12;
@@ -137,70 +142,6 @@ var RedactedMediaSchema = _v4.z.strictObject({
137
142
  path: ["url"]
138
143
  }).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
139
144
 
140
- // src/schemas/fill-in-the-blanks.ts
141
-
142
- var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
143
- var TextMatchPolicySchema = _v4.z.looseObject({
144
- caseSensitive: _v4.z.boolean().optional(),
145
- trim: _v4.z.boolean().optional(),
146
- normalize: _v4.z.enum(["none", "NFC", "NFKC"]).optional(),
147
- foldDiacritics: _v4.z.boolean().optional(),
148
- collapseInnerWhitespace: _v4.z.boolean().optional(),
149
- ignorePunctuation: _v4.z.boolean().optional(),
150
- levenshtein: _v4.z.number().int().min(0).optional(),
151
- locale: _v4.z.string().optional()
152
- });
153
- var BlankConfigSchema = _v4.z.looseObject({
154
- id: _v4.z.string().min(1),
155
- acceptedAnswers: _v4.z.array(
156
- _v4.z.string().min(1).refine((answer) => answer.trim().length > 0, {
157
- error: "Accepted answers must contain non-whitespace characters."
158
- })
159
- ).min(1),
160
- caseSensitive: _v4.z.boolean().optional(),
161
- trimWhitespace: _v4.z.boolean().optional(),
162
- match: TextMatchPolicySchema.optional(),
163
- hint: _v4.z.string().optional(),
164
- feedback: _v4.z.string().optional()
165
- });
166
- var FillInTheBlanksDataSchema = _v4.z.looseObject({
167
- schemaVersion: _v4.z.literal("1.0"),
168
- type: _v4.z.literal("fill-in-the-blanks"),
169
- id: _v4.z.string().min(1),
170
- title: _v4.z.string().min(1),
171
- passage: _v4.z.string().min(1),
172
- passageHtml: _v4.z.string().optional(),
173
- blanks: _v4.z.array(BlankConfigSchema).min(1),
174
- scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
175
- media: MediaSchema.optional(),
176
- feedback: FeedbackSchema.optional(),
177
- passThreshold: _v4.z.number().min(0).max(1).optional(),
178
- locale: _v4.z.string().optional(),
179
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
180
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
181
- ai: AiPermissionsSchema.optional()
182
- }).refine(
183
- (data) => {
184
- const placeholderCounts2 = /* @__PURE__ */ new Map();
185
- for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
186
- const id = match[1];
187
- placeholderCounts2.set(id, (_nullishCoalesce(placeholderCounts2.get(id), () => ( 0))) + 1);
188
- }
189
- const blankIds = data.blanks.map((blank) => blank.id);
190
- if (new Set(blankIds).size !== blankIds.length) {
191
- return false;
192
- }
193
- if (placeholderCounts2.size !== blankIds.length) {
194
- return false;
195
- }
196
- return blankIds.every((id) => placeholderCounts2.get(id) === 1);
197
- },
198
- {
199
- error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
200
- path: ["passage"]
201
- }
202
- );
203
-
204
145
  // src/validation-scope.ts
205
146
  var active = null;
206
147
  function withValidationScope(run) {
@@ -1306,112 +1247,428 @@ var DeferredScoringError = class extends Error {
1306
1247
 
1307
1248
  };
1308
1249
 
1309
- // src/schemas/dictation.ts
1250
+ // src/schemas/multiple-choice.ts
1310
1251
 
1311
- var DictationSlowMediaSchema = _v4.z.looseObject({
1312
- type: _v4.z.literal("audio"),
1313
- url: MediaUrlSchema,
1314
- alt: _v4.z.string().min(1).optional()
1252
+
1253
+ // src/schemas/ai.ts
1254
+
1255
+ var AiPermissionsSchema = _v4.z.looseObject({
1256
+ explanations: _v4.z.boolean().optional(),
1257
+ hints: _v4.z.boolean().optional()
1315
1258
  });
1316
- var RuleTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1317
- var TranscriptTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1318
- var DictationEquivalenceSchema = _v4.z.looseObject({
1319
- from: RuleTextSchema,
1320
- to: RuleTextSchema
1321
- }).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1322
- error: `A rule rewrites at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1323
- path: ["from"]
1324
- }).refine((rule) => codePointLength(rule.to) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
1325
- error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
1326
- path: ["to"]
1259
+
1260
+ // src/schemas/feedback.ts
1261
+
1262
+ var FeedbackSchema = _v4.z.looseObject({
1263
+ correct: _v4.z.string().min(1).optional(),
1264
+ incorrect: _v4.z.string().min(1).optional()
1327
1265
  });
1328
- var DictationToleranceSchema = _v4.z.looseObject({
1329
- equivalences: _v4.z.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
1330
- error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
1331
- }).optional()
1266
+
1267
+ // src/schemas/multiple-choice.ts
1268
+ var MultipleChoiceOptionMediaSchema = _v4.z.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: _v4.z.enum(["image", "audio"]),
1273
+ url: MediaUrlSchema,
1274
+ alt: _v4.z.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"]
1332
1282
  });
1333
- var normalizers = /* @__PURE__ */ new WeakMap();
1334
- function normalizerFor(data) {
1335
- let normalizer = normalizers.get(data);
1336
- if (normalizer === void 0) {
1337
- normalizer = dictationNormalizer(data.tolerance);
1338
- normalizers.set(data, normalizer);
1339
- }
1340
- return normalizer;
1341
- }
1342
- function measure(data, text) {
1343
- return normalizerFor(data).measure(text);
1344
- }
1345
- function candidatesOf(data) {
1346
- return [data.transcript, ..._nullishCoalesce(data.acceptedTranscripts, () => ( []))];
1347
- }
1348
- function isEmpty(measured) {
1349
- return measured.normalized === "";
1350
- }
1351
- function checkEvenAfterIssues(check) {
1352
- const guard = new _v4.z.core.$ZodCheck({ check: "custom", when: () => true });
1353
- guard._zod.check = check;
1354
- return guard;
1355
- }
1356
- var DictationDataShape = _v4.z.looseObject({
1283
+ var MultipleChoiceOptionSchema = _v4.z.looseObject({
1284
+ id: _v4.z.string().min(1),
1285
+ text: _v4.z.string().min(1),
1286
+ isCorrect: _v4.z.boolean(),
1287
+ feedback: _v4.z.string().optional(),
1288
+ media: MultipleChoiceOptionMediaSchema.optional()
1289
+ });
1290
+ var MultipleChoiceDataSchema = _v4.z.looseObject({
1357
1291
  schemaVersion: _v4.z.literal("1.0"),
1358
- type: _v4.z.literal("dictation"),
1292
+ type: _v4.z.literal("multiple-choice"),
1359
1293
  id: _v4.z.string().min(1),
1360
1294
  title: _v4.z.string().min(1),
1361
- transcript: TranscriptTextSchema,
1362
- acceptedTranscripts: _v4.z.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1295
+ question: _v4.z.string().min(1),
1296
+ questionHtml: _v4.z.string().optional(),
1297
+ mode: _v4.z.enum(["single", "multi"]),
1298
+ options: _v4.z.array(MultipleChoiceOptionSchema).min(2).max(26),
1299
+ scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
1363
1300
  media: MediaSchema.optional(),
1364
- slowMedia: DictationSlowMediaSchema.optional(),
1365
- hints: _v4.z.looseObject({ mode: _v4.z.literal("progressive-words") }).optional(),
1366
- tolerance: DictationToleranceSchema.optional(),
1367
1301
  feedback: FeedbackSchema.optional(),
1368
1302
  passThreshold: _v4.z.number().min(0).max(1).optional(),
1303
+ shuffle: _v4.z.boolean().optional(),
1369
1304
  locale: _v4.z.string().optional(),
1370
1305
  learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1371
1306
  difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1372
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"]
1373
1320
  });
1374
- var DictationDataSchema = DictationDataShape.check(
1375
- checkEvenAfterIssues((ctx) => {
1376
- if (ctx.issues.some((issue2) => (_nullishCoalesce(_optionalChain([issue2, 'access', _33 => _33.path, 'optionalAccess', _34 => _34.length]), () => ( 0))) === 0)) {
1377
- return;
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;
1378
1358
  }
1379
- const data = ctx.value;
1380
- const refused = new Set(ctx.issues.map((issue2) => String(_optionalChain([issue2, 'access', _35 => _35.path, 'optionalAccess', _36 => _36[0]]))));
1381
- const report = (path, input, message) => {
1382
- ctx.issues.push({ code: "custom", input, message, path: [...path] });
1359
+ node = node[segment];
1360
+ }
1361
+ return node;
1362
+ }
1363
+ var isThenable = (value) => typeof _optionalChain([value, 'optionalAccess', _33 => _33.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
+ _optionalChain([options, 'optionalAccess', _34 => _34.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
+ }))
1383
1384
  };
1384
- const rulesParse = !refused.has("tolerance");
1385
- const transcriptParses = !refused.has("transcript");
1386
- if (rulesParse && transcriptParses) {
1387
- const transcript = measure(data, data.transcript);
1388
- if (isEmpty(transcript)) {
1389
- report(
1390
- ["transcript"],
1391
- data.transcript,
1392
- "The transcript must contain something to type: after ignoring case, punctuation and spacing nothing is left."
1393
- );
1394
- }
1395
- if (transcript.tooLong) {
1396
- report(
1397
- ["transcript"],
1398
- data.transcript,
1399
- `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.`
1400
- );
1401
- }
1402
- if (!refused.has("acceptedTranscripts")) {
1403
- const accepted = _nullishCoalesce(data.acceptedTranscripts, () => ( []));
1404
- if (accepted.some((entry) => {
1405
- const measured = measure(data, entry);
1406
- return measured.tooLong || isEmpty(measured);
1407
- })) {
1408
- report(
1409
- ["acceptedTranscripts"],
1410
- data.acceptedTranscripts,
1411
- `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.`
1412
- );
1413
- }
1414
- const normalized = candidatesOf(data).map((entry) => measure(data, entry).normalized).filter((entry) => entry !== null);
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 = (_nullishCoalesce(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 = _optionalChain([value, 'optionalAccess', _35 => _35["~standard"]]);
1416
+ return _optionalChain([props, 'optionalAccess', _36 => _36.version]) === 1 && typeof props.validate === "function";
1417
+ }
1418
+ function standardJsonSchemaOf(schema) {
1419
+ const converter = schema["~standard"].jsonSchema;
1420
+ if (typeof _optionalChain([converter, 'optionalAccess', _37 => _37.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 = _nullishCoalesce(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
+
1556
+
1557
+ // src/schemas/after-issues.ts
1558
+
1559
+ function checkEvenAfterIssues(check) {
1560
+ const guard = new _v4.z.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(_optionalChain([issue2, 'access', _38 => _38.path, 'optionalAccess', _39 => _39[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 = _v4.z.looseObject({
1575
+ type: _v4.z.literal("audio"),
1576
+ url: MediaUrlSchema,
1577
+ alt: _v4.z.string().min(1).optional()
1578
+ });
1579
+ var RuleTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1580
+ var TranscriptTextSchema = _v4.z.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1581
+ var DictationEquivalenceSchema = _v4.z.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 = _v4.z.looseObject({
1592
+ equivalences: _v4.z.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, ..._nullishCoalesce(data.acceptedTranscripts, () => ( []))];
1610
+ }
1611
+ function isEmpty(measured) {
1612
+ return measured.normalized === "";
1613
+ }
1614
+ var DictationDataShape = _v4.z.looseObject({
1615
+ schemaVersion: _v4.z.literal("1.0"),
1616
+ type: _v4.z.literal("dictation"),
1617
+ id: _v4.z.string().min(1),
1618
+ title: _v4.z.string().min(1),
1619
+ transcript: TranscriptTextSchema,
1620
+ acceptedTranscripts: _v4.z.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1621
+ media: MediaSchema.optional(),
1622
+ slowMedia: DictationSlowMediaSchema.optional(),
1623
+ hints: _v4.z.looseObject({ mode: _v4.z.literal("progressive-words") }).optional(),
1624
+ tolerance: DictationToleranceSchema.optional(),
1625
+ feedback: FeedbackSchema.optional(),
1626
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
1627
+ locale: _v4.z.string().optional(),
1628
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1629
+ difficultyLevel: _v4.z.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) => (_nullishCoalesce(_optionalChain([issue2, 'access', _40 => _40.path, 'optionalAccess', _41 => _41.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 = _nullishCoalesce(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);
1415
1672
  if (new Set(normalized).size !== normalized.length) {
1416
1673
  report(
1417
1674
  ["acceptedTranscripts"],
@@ -1430,7 +1687,7 @@ var DictationDataSchema = DictationDataShape.check(
1430
1687
  "A dictation is dictated: its recording must be audio."
1431
1688
  );
1432
1689
  }
1433
- if (slowParses && _optionalChain([data, 'access', _37 => _37.slowMedia, 'optionalAccess', _38 => _38.playback]) !== void 0) {
1690
+ if (slowParses && _optionalChain([data, 'access', _42 => _42.slowMedia, 'optionalAccess', _43 => _43.playback]) !== void 0) {
1434
1691
  report(
1435
1692
  ["slowMedia", "playback"],
1436
1693
  data.slowMedia.playback,
@@ -1438,14 +1695,14 @@ var DictationDataSchema = DictationDataShape.check(
1438
1695
  );
1439
1696
  }
1440
1697
  if (mediaParses && slowParses && data.slowMedia !== void 0) {
1441
- if (_optionalChain([data, 'access', _39 => _39.media, 'optionalAccess', _40 => _40.playback, 'optionalAccess', _41 => _41.maxPlays]) !== void 0) {
1698
+ if (_optionalChain([data, 'access', _44 => _44.media, 'optionalAccess', _45 => _45.playback, 'optionalAccess', _46 => _46.maxPlays]) !== void 0) {
1442
1699
  report(
1443
1700
  ["slowMedia"],
1444
1701
  data.slowMedia,
1445
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`)."
1446
1703
  );
1447
1704
  }
1448
- if (typeof data.slowMedia.url === "string" && typeof _optionalChain([data, 'access', _42 => _42.media, 'optionalAccess', _43 => _43.url]) === "string" && data.slowMedia.url === data.media.url) {
1705
+ if (typeof data.slowMedia.url === "string" && typeof _optionalChain([data, 'access', _47 => _47.media, 'optionalAccess', _48 => _48.url]) === "string" && data.slowMedia.url === data.media.url) {
1449
1706
  report(
1450
1707
  ["slowMedia", "url"],
1451
1708
  data.slowMedia.url,
@@ -1460,12 +1717,12 @@ var DictationDataSchema = DictationDataShape.check(
1460
1717
  );
1461
1718
  }
1462
1719
  }
1463
- if (rulesParse && !(_nullishCoalesce(_optionalChain([data, 'access', _44 => _44.tolerance, 'optionalAccess', _45 => _45.equivalences]), () => ( []))).every(
1720
+ if (rulesParse && !(_nullishCoalesce(_optionalChain([data, 'access', _49 => _49.tolerance, 'optionalAccess', _50 => _50.equivalences]), () => ( []))).every(
1464
1721
  (rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
1465
1722
  )) {
1466
1723
  report(
1467
1724
  ["tolerance", "equivalences"],
1468
- _optionalChain([data, 'access', _46 => _46.tolerance, 'optionalAccess', _47 => _47.equivalences]),
1725
+ _optionalChain([data, 'access', _51 => _51.tolerance, 'optionalAccess', _52 => _52.equivalences]),
1469
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)."
1470
1727
  );
1471
1728
  }
@@ -1474,7 +1731,7 @@ var DictationDataSchema = DictationDataShape.check(
1474
1731
  for (const captions of captionFieldsOf(recording)) {
1475
1732
  report(
1476
1733
  [field, captions],
1477
- _optionalChain([recording, 'optionalAccess', _48 => _48[captions]]),
1734
+ _optionalChain([recording, 'optionalAccess', _53 => _53[captions]]),
1478
1735
  "A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
1479
1736
  );
1480
1737
  }
@@ -1486,8 +1743,8 @@ var DictationDataSchema = DictationDataShape.check(
1486
1743
  const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
1487
1744
  const shown = [
1488
1745
  [["title"], refused.has("title") ? void 0 : data.title],
1489
- [["media", "alt"], _optionalChain([data, 'access', _49 => _49.media, 'optionalAccess', _50 => _50.alt])],
1490
- [["slowMedia", "alt"], _optionalChain([data, 'access', _51 => _51.slowMedia, 'optionalAccess', _52 => _52.alt])]
1746
+ [["media", "alt"], _optionalChain([data, 'access', _54 => _54.media, 'optionalAccess', _55 => _55.alt])],
1747
+ [["slowMedia", "alt"], _optionalChain([data, 'access', _56 => _56.slowMedia, 'optionalAccess', _57 => _57.alt])]
1491
1748
  ];
1492
1749
  for (const [path, value] of shown) {
1493
1750
  if (value === void 0) {
@@ -1539,671 +1796,6 @@ function groupCaptionsField(group, written) {
1539
1796
  }
1540
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.";
1541
1798
 
1542
- // src/schemas/multiple-choice.ts
1543
-
1544
- var MultipleChoiceOptionMediaSchema = _v4.z.looseObject({
1545
- // No `embed`: an iframe swallows the click that selects the option, so the
1546
- // learner could not choose it. No `video`: a native control bar inside the
1547
- // option's label eats the same click.
1548
- type: _v4.z.enum(["image", "audio"]),
1549
- url: MediaUrlSchema,
1550
- alt: _v4.z.string().min(1).optional(),
1551
- captionsUrl: MediaUrlSchema.optional()
1552
- }).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
1553
- error: "An image option requires a non-empty `alt`.",
1554
- path: ["alt"]
1555
- }).refine((media) => !("playback" in media), {
1556
- error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
1557
- path: ["playback"]
1558
- });
1559
- var MultipleChoiceOptionSchema = _v4.z.looseObject({
1560
- id: _v4.z.string().min(1),
1561
- text: _v4.z.string().min(1),
1562
- isCorrect: _v4.z.boolean(),
1563
- feedback: _v4.z.string().optional(),
1564
- media: MultipleChoiceOptionMediaSchema.optional()
1565
- });
1566
- var MultipleChoiceDataSchema = _v4.z.looseObject({
1567
- schemaVersion: _v4.z.literal("1.0"),
1568
- type: _v4.z.literal("multiple-choice"),
1569
- id: _v4.z.string().min(1),
1570
- title: _v4.z.string().min(1),
1571
- question: _v4.z.string().min(1),
1572
- questionHtml: _v4.z.string().optional(),
1573
- mode: _v4.z.enum(["single", "multi"]),
1574
- options: _v4.z.array(MultipleChoiceOptionSchema).min(2).max(26),
1575
- scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
1576
- media: MediaSchema.optional(),
1577
- feedback: FeedbackSchema.optional(),
1578
- passThreshold: _v4.z.number().min(0).max(1).optional(),
1579
- shuffle: _v4.z.boolean().optional(),
1580
- locale: _v4.z.string().optional(),
1581
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1582
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1583
- ai: AiPermissionsSchema.optional()
1584
- }).refine((data) => data.options.some((option) => option.isCorrect), {
1585
- error: "At least one option must be marked correct.",
1586
- path: ["options"]
1587
- }).refine(
1588
- (data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
1589
- {
1590
- error: 'Single-select activities (mode: "single") must have exactly one correct option.',
1591
- path: ["options"]
1592
- }
1593
- ).refine((data) => new Set(data.options.map((option) => option.id)).size === data.options.length, {
1594
- error: "Option ids must be unique within the activity.",
1595
- path: ["options"]
1596
- });
1597
-
1598
- // src/scoring/speech/limits.ts
1599
- var READ_ALOUD_MAX_REFERENCE_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH;
1600
- var READ_ALOUD_MAX_SECONDS = 300;
1601
- var READ_ALOUD_MAX_TAKES = 20;
1602
- var READ_ALOUD_MAX_DIMENSION_WEIGHT = 1e3;
1603
- var SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
1604
- var SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
1605
-
1606
- // src/schemas/read-aloud.ts
1607
-
1608
-
1609
- // src/scoring/speech/locale.ts
1610
- var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
1611
-
1612
- // src/schemas/read-aloud.ts
1613
- var READ_ALOUD_DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
1614
- var READ_ALOUD_MAX_DIMENSIONS = READ_ALOUD_DIMENSIONS.length;
1615
- var ReadAloudSlowMediaSchema = _v4.z.strictObject({
1616
- type: _v4.z.literal("audio"),
1617
- url: MediaUrlSchema,
1618
- alt: _v4.z.string().min(1).optional()
1619
- });
1620
- var ReferenceTextSchema = _v4.z.string().min(1).refine((text) => text.trim() !== "", {
1621
- error: "The text to read aloud must contain something to read."
1622
- }).meta({ maxLength: READ_ALOUD_MAX_REFERENCE_LENGTH });
1623
- var RecordingBoundsSchema = _v4.z.looseObject({
1624
- maxSeconds: _v4.z.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
1625
- minSeconds: _v4.z.number().min(0).optional(),
1626
- maxTakes: _v4.z.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
1627
- });
1628
- var ReadAloudDimensionWeightSchema = _v4.z.looseObject({
1629
- name: _v4.z.enum(READ_ALOUD_DIMENSIONS),
1630
- weight: _v4.z.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
1631
- });
1632
- var ReadAloudScoringSchema = _v4.z.looseObject({
1633
- dimensions: _v4.z.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
1634
- });
1635
- function checkEvenAfterIssues2(check) {
1636
- const guard = new _v4.z.core.$ZodCheck({ check: "custom", when: () => true });
1637
- guard._zod.check = check;
1638
- return guard;
1639
- }
1640
- var ReadAloudDataShape = _v4.z.looseObject({
1641
- schemaVersion: _v4.z.literal("1.0"),
1642
- type: _v4.z.literal("read-aloud"),
1643
- id: _v4.z.string().min(1),
1644
- title: _v4.z.string().min(1),
1645
- instructions: _v4.z.string().optional(),
1646
- referenceText: ReferenceTextSchema,
1647
- locale: _v4.z.string().regex(CANONICAL_LOCALE_RE, {
1648
- 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.`
1649
- }),
1650
- media: MediaSchema.optional(),
1651
- slowMedia: ReadAloudSlowMediaSchema.optional(),
1652
- recording: RecordingBoundsSchema,
1653
- scoring: ReadAloudScoringSchema,
1654
- passThreshold: _v4.z.number().min(0).max(1).optional(),
1655
- feedback: FeedbackSchema.optional(),
1656
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1657
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1658
- ai: AiPermissionsSchema.optional()
1659
- });
1660
- var ReadAloudDataSchema = ReadAloudDataShape.check(
1661
- checkEvenAfterIssues2((ctx) => {
1662
- if (ctx.issues.some((issue2) => (_nullishCoalesce(_optionalChain([issue2, 'access', _53 => _53.path, 'optionalAccess', _54 => _54.length]), () => ( 0))) === 0)) {
1663
- return;
1664
- }
1665
- const data = ctx.value;
1666
- const refused = new Set(ctx.issues.map((issue2) => String(_optionalChain([issue2, 'access', _55 => _55.path, 'optionalAccess', _56 => _56[0]]))));
1667
- const report = (path, input, message) => {
1668
- ctx.issues.push({ code: "custom", input, message, path: [...path] });
1669
- };
1670
- if (!refused.has("referenceText")) {
1671
- const text = data.referenceText;
1672
- const measured = dictationNormalizer(void 0).measure(text);
1673
- if (measured.tooLong) {
1674
- report(
1675
- ["referenceText"],
1676
- text,
1677
- `The text to read aloud is at most ${READ_ALOUD_MAX_REFERENCE_LENGTH} characters, before and after normalisation.`
1678
- );
1679
- }
1680
- if (measured.normalized === "") {
1681
- report(
1682
- ["referenceText"],
1683
- text,
1684
- "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."
1685
- );
1686
- }
1687
- if (containsUnspacedScript(text)) {
1688
- report(
1689
- ["referenceText"],
1690
- text,
1691
- "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."
1692
- );
1693
- }
1694
- }
1695
- const mediaParses = !refused.has("media");
1696
- const slowParses = !refused.has("slowMedia");
1697
- if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
1698
- report(
1699
- ["media", "type"],
1700
- data.media.type,
1701
- "A read-aloud model is read aloud: its recording must be audio."
1702
- );
1703
- }
1704
- if (mediaParses && slowParses && data.slowMedia !== void 0) {
1705
- if (data.media === void 0) {
1706
- report(
1707
- ["media"],
1708
- data.media,
1709
- "A slow model recording accompanies a recording: add `media` first."
1710
- );
1711
- }
1712
- if (_optionalChain([data, 'access', _57 => _57.media, 'optionalAccess', _58 => _58.playback, 'optionalAccess', _59 => _59.maxPlays]) !== void 0) {
1713
- report(
1714
- ["slowMedia"],
1715
- data.slowMedia,
1716
- "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays`."
1717
- );
1718
- }
1719
- if (typeof data.slowMedia.url === "string" && typeof _optionalChain([data, 'access', _60 => _60.media, 'optionalAccess', _61 => _61.url]) === "string" && data.slowMedia.url === data.media.url) {
1720
- report(
1721
- ["slowMedia", "url"],
1722
- data.slowMedia.url,
1723
- "The slow model recording must be a different file from the recording."
1724
- );
1725
- }
1726
- }
1727
- if (!refused.has("scoring")) {
1728
- const dimensions = data.scoring.dimensions;
1729
- const seen = /* @__PURE__ */ new Set();
1730
- for (const [index, dimension] of dimensions.entries()) {
1731
- if (seen.has(dimension.name)) {
1732
- report(
1733
- ["scoring", "dimensions", index, "name"],
1734
- dimension.name,
1735
- `"${dimension.name}" is already weighed: a dimension counted twice hides which weight the grade used.`
1736
- );
1737
- }
1738
- seen.add(dimension.name);
1739
- }
1740
- if (dimensions.every((dimension) => dimension.weight === 0)) {
1741
- report(
1742
- ["scoring", "dimensions"],
1743
- dimensions,
1744
- "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."
1745
- );
1746
- }
1747
- }
1748
- if (!refused.has("recording")) {
1749
- const { minSeconds, maxSeconds } = data.recording;
1750
- if (minSeconds !== void 0 && minSeconds >= maxSeconds) {
1751
- report(
1752
- ["recording", "minSeconds"],
1753
- minSeconds,
1754
- "The shortest take must be shorter than the longest."
1755
- );
1756
- }
1757
- }
1758
- })
1759
- );
1760
-
1761
- // src/count-words.ts
1762
- function countWords(text) {
1763
- if (typeof text !== "string") {
1764
- return 0;
1765
- }
1766
- const trimmed = text.trim();
1767
- if (trimmed === "") {
1768
- return 0;
1769
- }
1770
- return trimmed.split(/\s+/).length;
1771
- }
1772
-
1773
- // src/schemas/gap-select.ts
1774
-
1775
- var GapSelectChoiceSchema = _v4.z.looseObject({
1776
- id: _v4.z.string().min(1),
1777
- text: _v4.z.string().min(1)
1778
- });
1779
- var GapSelectBankSchema = _v4.z.looseObject({
1780
- id: _v4.z.string().min(1),
1781
- choices: _v4.z.array(GapSelectChoiceSchema).min(2)
1782
- });
1783
- var GapSelectGapSchema = _v4.z.looseObject({
1784
- id: _v4.z.string().min(1),
1785
- choices: _v4.z.array(GapSelectChoiceSchema).min(2).optional(),
1786
- bankId: _v4.z.string().min(1).optional(),
1787
- correctChoiceId: _v4.z.string().min(1),
1788
- feedback: _v4.z.string().optional()
1789
- });
1790
- function resolveChoices(gap, banks) {
1791
- if (gap.choices !== void 0) {
1792
- return gap.bankId === void 0 ? gap.choices : void 0;
1793
- }
1794
- if (gap.bankId === void 0) {
1795
- return void 0;
1796
- }
1797
- return _optionalChain([banks, 'access', _62 => _62.find, 'call', _63 => _63((bank) => bank.id === gap.bankId), 'optionalAccess', _64 => _64.choices]);
1798
- }
1799
- function placeholderCounts(passage) {
1800
- const counts = /* @__PURE__ */ new Map();
1801
- for (const match of passage.matchAll(PLACEHOLDER_RE)) {
1802
- const id = match[1];
1803
- counts.set(id, (_nullishCoalesce(counts.get(id), () => ( 0))) + 1);
1804
- }
1805
- return counts;
1806
- }
1807
- var GapSelectDataSchema = _v4.z.looseObject({
1808
- schemaVersion: _v4.z.literal("1.0"),
1809
- type: _v4.z.literal("gap-select"),
1810
- id: _v4.z.string().min(1),
1811
- title: _v4.z.string().min(1),
1812
- passage: _v4.z.string().min(1),
1813
- passageHtml: _v4.z.string().optional(),
1814
- gaps: _v4.z.array(GapSelectGapSchema).min(1),
1815
- banks: _v4.z.array(GapSelectBankSchema).optional(),
1816
- scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
1817
- presentation: _v4.z.literal("dropdown").optional(),
1818
- shuffleChoices: _v4.z.boolean().optional(),
1819
- media: MediaSchema.optional(),
1820
- feedback: FeedbackSchema.optional(),
1821
- passThreshold: _v4.z.number().min(0).max(1).optional(),
1822
- locale: _v4.z.string().optional(),
1823
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1824
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1825
- ai: AiPermissionsSchema.optional()
1826
- }).refine(
1827
- (data) => {
1828
- const ids = (_nullishCoalesce(data.banks, () => ( []))).map((bank) => bank.id);
1829
- return new Set(ids).size === ids.length;
1830
- },
1831
- { error: "Word bank ids must be unique.", path: ["banks"] }
1832
- ).refine(
1833
- (data) => {
1834
- const ids = data.gaps.map((gap) => gap.id);
1835
- return new Set(ids).size === ids.length;
1836
- },
1837
- { error: "Gap ids must be unique.", path: ["gaps"] }
1838
- ).refine(
1839
- (data) => {
1840
- const counts = placeholderCounts(data.passage);
1841
- const ids = data.gaps.map((gap) => gap.id);
1842
- return counts.size === new Set(ids).size && ids.every((id) => counts.get(id) === 1) && [...counts.values()].every((count) => count === 1);
1843
- },
1844
- {
1845
- error: "Every gap must appear exactly once in the passage as {{gap_id}}, and every {{gap_id}} must have a gap.",
1846
- path: ["passage"]
1847
- }
1848
- ).refine(
1849
- (data) => data.gaps.every((gap) => gap.choices === void 0 !== (gap.bankId === void 0)),
1850
- {
1851
- error: "Each gap needs exactly one choice source: its own `choices`, or a `bankId`.",
1852
- path: ["gaps"]
1853
- }
1854
- ).refine(
1855
- (data) => {
1856
- const bankIds = new Set((_nullishCoalesce(data.banks, () => ( []))).map((bank) => bank.id));
1857
- return data.gaps.every((gap) => gap.bankId === void 0 || bankIds.has(gap.bankId));
1858
- },
1859
- { error: "A gap references a `bankId` that no word bank defines.", path: ["gaps"] }
1860
- ).refine(
1861
- (data) => {
1862
- const banks = _nullishCoalesce(data.banks, () => ( []));
1863
- return data.gaps.every((gap) => {
1864
- const choices = resolveChoices(gap, banks);
1865
- if (choices === void 0) {
1866
- return true;
1867
- }
1868
- const ids = choices.map((choice) => choice.id);
1869
- return new Set(ids).size === ids.length && ids.includes(gap.correctChoiceId);
1870
- });
1871
- },
1872
- {
1873
- error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
1874
- path: ["gaps"]
1875
- }
1876
- );
1877
-
1878
- // src/schemas/written-response.ts
1879
-
1880
- var WrittenResponseRubricCriterionSchema = _v4.z.looseObject({
1881
- name: _v4.z.string().min(1),
1882
- description: _v4.z.string().optional(),
1883
- weight: _v4.z.number().min(0)
1884
- });
1885
- var WrittenResponseRubricSchema = _v4.z.looseObject({
1886
- label: _v4.z.string().optional(),
1887
- criteria: _v4.z.array(WrittenResponseRubricCriterionSchema).min(1)
1888
- });
1889
- var RedactedWrittenResponseRubricCriterionSchema = _v4.z.strictObject({
1890
- name: _v4.z.string().min(1),
1891
- description: _v4.z.string().optional(),
1892
- weight: _v4.z.number().min(0)
1893
- });
1894
- var RedactedWrittenResponseRubricSchema = _v4.z.strictObject({
1895
- label: _v4.z.string().optional(),
1896
- criteria: _v4.z.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
1897
- });
1898
- var WrittenResponseDataSchema = _v4.z.looseObject({
1899
- schemaVersion: _v4.z.literal("1.0"),
1900
- type: _v4.z.literal("written-response"),
1901
- id: _v4.z.string().min(1),
1902
- title: _v4.z.string().min(1),
1903
- prompt: _v4.z.string(),
1904
- promptHtml: _v4.z.string().optional(),
1905
- minWords: _v4.z.number().int().min(0),
1906
- maxWords: _v4.z.number().int().min(1),
1907
- rubric: WrittenResponseRubricSchema.optional(),
1908
- languageTarget: _v4.z.string().optional(),
1909
- media: MediaSchema.optional(),
1910
- feedback: FeedbackSchema.optional(),
1911
- passThreshold: _v4.z.number().min(0).max(1).optional(),
1912
- locale: _v4.z.string().optional(),
1913
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1914
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1915
- ai: AiPermissionsSchema.optional()
1916
- }).refine((data) => data.maxWords >= data.minWords, {
1917
- error: "maxWords must be greater than or equal to minWords.",
1918
- path: ["maxWords"]
1919
- });
1920
-
1921
- // src/schemas/redacted.ts
1922
-
1923
- var redactedBase = {
1924
- /** Marker distinguishing a redacted projection from full activity data. */
1925
- redacted: _v4.z.literal(true),
1926
- /** Slot identity, carried through redaction so the client and the plan agree. */
1927
- slotKey: _v4.z.string().min(1).optional(),
1928
- schemaVersion: _v4.z.literal("1.0"),
1929
- id: _v4.z.string().min(1),
1930
- title: _v4.z.string().min(1),
1931
- media: RedactedMediaSchema.optional(),
1932
- passThreshold: _v4.z.number().min(0).max(1).optional(),
1933
- locale: _v4.z.string().optional(),
1934
- learningObjectives: _v4.z.array(_v4.z.string()).optional(),
1935
- difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
1936
- ai: _v4.z.strictObject({ explanations: _v4.z.boolean().optional(), hints: _v4.z.boolean().optional() }).optional()
1937
- };
1938
- var RedactedMultipleChoiceOptionMediaSchema = _v4.z.strictObject({
1939
- type: _v4.z.enum(["image", "audio"]),
1940
- url: _v4.z.string().min(1),
1941
- alt: _v4.z.string().min(1).optional(),
1942
- captionsUrl: _v4.z.string().min(1).optional()
1943
- });
1944
- var RedactedMultipleChoiceOptionSchema = _v4.z.strictObject({
1945
- id: _v4.z.string().min(1),
1946
- text: _v4.z.string().min(1),
1947
- media: RedactedMultipleChoiceOptionMediaSchema.optional()
1948
- });
1949
- var RedactedMultipleChoiceDataSchema = _v4.z.strictObject({
1950
- ...redactedBase,
1951
- type: _v4.z.literal("multiple-choice"),
1952
- question: _v4.z.string().min(1),
1953
- questionHtml: _v4.z.string().optional(),
1954
- mode: _v4.z.enum(["single", "multi"]),
1955
- options: _v4.z.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
1956
- shuffle: _v4.z.boolean().optional()
1957
- });
1958
- var RedactedBlankConfigSchema = _v4.z.strictObject({
1959
- id: _v4.z.string().min(1),
1960
- hint: _v4.z.string().optional()
1961
- });
1962
- var RedactedFillInTheBlanksDataSchema = _v4.z.strictObject({
1963
- ...redactedBase,
1964
- type: _v4.z.literal("fill-in-the-blanks"),
1965
- passage: _v4.z.string().min(1),
1966
- passageHtml: _v4.z.string().optional(),
1967
- blanks: _v4.z.array(RedactedBlankConfigSchema).min(1)
1968
- });
1969
- var RedactedGapSelectChoiceSchema = _v4.z.strictObject({
1970
- id: _v4.z.string().min(1),
1971
- text: _v4.z.string().min(1)
1972
- });
1973
- var RedactedGapSelectBankSchema = _v4.z.strictObject({
1974
- id: _v4.z.string().min(1),
1975
- choices: _v4.z.array(RedactedGapSelectChoiceSchema).min(2)
1976
- });
1977
- var RedactedGapSelectGapSchema = _v4.z.strictObject({
1978
- id: _v4.z.string().min(1),
1979
- choices: _v4.z.array(RedactedGapSelectChoiceSchema).min(2).optional(),
1980
- bankId: _v4.z.string().min(1).optional()
1981
- });
1982
- var RedactedGapSelectDataSchema = _v4.z.strictObject({
1983
- ...redactedBase,
1984
- type: _v4.z.literal("gap-select"),
1985
- passage: _v4.z.string().min(1),
1986
- passageHtml: _v4.z.string().optional(),
1987
- gaps: _v4.z.array(RedactedGapSelectGapSchema).min(1),
1988
- banks: _v4.z.array(RedactedGapSelectBankSchema).optional(),
1989
- presentation: _v4.z.literal("dropdown").optional(),
1990
- shuffleChoices: _v4.z.boolean().optional()
1991
- });
1992
- var RedactedWrittenResponseDataSchema = _v4.z.strictObject({
1993
- ...redactedBase,
1994
- type: _v4.z.literal("written-response"),
1995
- prompt: _v4.z.string(),
1996
- promptHtml: _v4.z.string().optional(),
1997
- minWords: _v4.z.number().int().min(0),
1998
- maxWords: _v4.z.number().int().min(1),
1999
- rubric: RedactedWrittenResponseRubricSchema.optional(),
2000
- languageTarget: _v4.z.string().optional()
2001
- });
2002
- var RedactedDictationSlowMediaSchema = _v4.z.strictObject({
2003
- type: _v4.z.literal("audio"),
2004
- url: MediaUrlSchema,
2005
- alt: _v4.z.string().min(1).optional()
2006
- });
2007
- var RedactedDictationDataSchema = _v4.z.strictObject({
2008
- ...redactedBase,
2009
- type: _v4.z.literal("dictation"),
2010
- slowMedia: RedactedDictationSlowMediaSchema.optional(),
2011
- hints: _v4.z.strictObject({ mode: _v4.z.literal("progressive-words") }).optional()
2012
- }).check((ctx) => {
2013
- const data = ctx.value;
2014
- for (const captions of captionFieldsOf(data.media)) {
2015
- ctx.issues.push({
2016
- code: "custom",
2017
- input: _optionalChain([data, 'access', _65 => _65.media, 'optionalAccess', _66 => _66[captions]]),
2018
- message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
2019
- path: ["media", captions]
2020
- });
2021
- }
2022
- if (data.slowMedia !== void 0 && _optionalChain([data, 'access', _67 => _67.media, 'optionalAccess', _68 => _68.playback, 'optionalAccess', _69 => _69.maxPlays]) !== void 0) {
2023
- ctx.issues.push({
2024
- code: "custom",
2025
- input: data.slowMedia,
2026
- message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
2027
- path: ["slowMedia"]
2028
- });
2029
- }
2030
- if (data.slowMedia !== void 0 && data.media === void 0) {
2031
- ctx.issues.push({
2032
- code: "custom",
2033
- input: data.slowMedia,
2034
- message: "A slow recording accompanies a recording: this payload has no `media`.",
2035
- path: ["media"]
2036
- });
2037
- }
2038
- });
2039
- var RedactedReadAloudDataSchema = _v4.z.strictObject({
2040
- ...redactedBase,
2041
- type: _v4.z.literal("read-aloud"),
2042
- locale: _v4.z.string().regex(CANONICAL_LOCALE_RE),
2043
- instructions: _v4.z.string().optional(),
2044
- referenceText: _v4.z.string().min(1),
2045
- slowMedia: ReadAloudSlowMediaSchema.optional(),
2046
- recording: _v4.z.strictObject({
2047
- maxSeconds: _v4.z.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
2048
- minSeconds: _v4.z.number().min(0).optional(),
2049
- maxTakes: _v4.z.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
2050
- }),
2051
- scoring: _v4.z.strictObject({
2052
- dimensions: _v4.z.array(
2053
- _v4.z.strictObject({
2054
- name: _v4.z.enum(READ_ALOUD_DIMENSIONS),
2055
- weight: _v4.z.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
2056
- })
2057
- ).min(1).max(READ_ALOUD_DIMENSIONS.length)
2058
- })
2059
- }).check((ctx) => {
2060
- const data = ctx.value;
2061
- if (data.media !== void 0 && data.media.type !== "audio") {
2062
- ctx.issues.push({
2063
- code: "custom",
2064
- input: data.media.type,
2065
- message: "A read-aloud model recording must be audio, so this payload is not renderable.",
2066
- path: ["media", "type"]
2067
- });
2068
- }
2069
- if (data.slowMedia !== void 0 && data.media === void 0) {
2070
- ctx.issues.push({
2071
- code: "custom",
2072
- input: data.slowMedia,
2073
- message: "A slow model recording accompanies a recording: this payload has no `media`.",
2074
- path: ["media"]
2075
- });
2076
- }
2077
- if (data.slowMedia !== void 0 && _optionalChain([data, 'access', _70 => _70.media, 'optionalAccess', _71 => _71.playback, 'optionalAccess', _72 => _72.maxPlays]) !== void 0) {
2078
- ctx.issues.push({
2079
- code: "custom",
2080
- input: data.slowMedia,
2081
- message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
2082
- path: ["slowMedia"]
2083
- });
2084
- }
2085
- if (data.slowMedia !== void 0 && data.slowMedia.url === _optionalChain([data, 'access', _73 => _73.media, 'optionalAccess', _74 => _74.url])) {
2086
- ctx.issues.push({
2087
- code: "custom",
2088
- input: data.slowMedia.url,
2089
- message: "The slow model recording must be a different file from the recording.",
2090
- path: ["slowMedia", "url"]
2091
- });
2092
- }
2093
- });
2094
-
2095
- // src/scoring/text-match.ts
2096
- var COMBINING_MARKS_RE = /\p{M}/gu;
2097
- var PUNCTUATION_RE2 = /\p{P}/gu;
2098
- function applyBaseline(value, policy) {
2099
- let result = value;
2100
- if (policy.trim !== false) {
2101
- result = result.trim();
2102
- }
2103
- if (policy.caseSensitive !== true) {
2104
- result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
2105
- }
2106
- return result;
2107
- }
2108
- function applyNormalization(value, policy) {
2109
- let result = value;
2110
- if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
2111
- result = result.normalize(policy.normalize);
2112
- }
2113
- if (policy.ignorePunctuation === true) {
2114
- result = result.replace(PUNCTUATION_RE2, "");
2115
- }
2116
- if (policy.collapseInnerWhitespace === true) {
2117
- result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
2118
- if (policy.trim !== false) {
2119
- result = result.trim();
2120
- }
2121
- }
2122
- return result;
2123
- }
2124
- function applyDiacriticFold(value) {
2125
- return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
2126
- }
2127
- function levenshteinDistance(a, b, max) {
2128
- if (a === b) return 0;
2129
- if (Math.abs(a.length - b.length) > max) return max + 1;
2130
- if (a.length === 0) return b.length;
2131
- if (b.length === 0) return a.length;
2132
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
2133
- for (let i = 1; i <= a.length; i += 1) {
2134
- const current = [i];
2135
- let rowMin = i;
2136
- for (let j = 1; j <= b.length; j += 1) {
2137
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2138
- const value = Math.min(
2139
- previous[j] + 1,
2140
- current[j - 1] + 1,
2141
- previous[j - 1] + cost
2142
- );
2143
- current.push(value);
2144
- if (value < rowMin) rowMin = value;
2145
- }
2146
- if (rowMin > max) return max + 1;
2147
- previous = current;
2148
- }
2149
- return previous[b.length];
2150
- }
2151
- function matchText(input, accepted, policy = {}) {
2152
- const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
2153
- const baselineInput = applyBaseline(input, policy);
2154
- const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
2155
- if (baselineAccepted.some((answer) => answer === baselineInput)) {
2156
- return { matched: true, via: "exact" };
2157
- }
2158
- const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
2159
- const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
2160
- const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
2161
- if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
2162
- return { matched: true, via: "normalized" };
2163
- }
2164
- const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
2165
- const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
2166
- if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
2167
- return { matched: true, via: "folded" };
2168
- }
2169
- const maxDistance = _nullishCoalesce(policy.levenshtein, () => ( 0));
2170
- if (maxDistance > 0 && foldedInput.trim().length > 0 && foldedAccepted.some(
2171
- (answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
2172
- )) {
2173
- return { matched: true, via: "fuzzy" };
2174
- }
2175
- return { matched: false, via: "none" };
2176
- }
2177
-
2178
- // src/registry/registry.ts
2179
- var registry = /* @__PURE__ */ new Map();
2180
- function defineActivityType(descriptor) {
2181
- return descriptor;
2182
- }
2183
- function registerActivityType(descriptor) {
2184
- if (descriptor.type === "item-group") {
2185
- throw new Error(
2186
- `"item-group" is reserved for the SDK's item-group container (see ItemGroup) and cannot be registered as an activity type.`
2187
- );
2188
- }
2189
- const existing = registry.get(descriptor.type);
2190
- if (existing !== void 0) {
2191
- if (existing === descriptor) {
2192
- return;
2193
- }
2194
- throw new Error(
2195
- `Activity type "${descriptor.type}" is already registered. Registering a different descriptor for an existing type is not allowed.`
2196
- );
2197
- }
2198
- registry.set(descriptor.type, descriptor);
2199
- }
2200
- function getActivityTypeDescriptor(type) {
2201
- return registry.get(type);
2202
- }
2203
- function registeredActivityTypes() {
2204
- return [...registry.keys()];
2205
- }
2206
-
2207
1799
  // src/authoring/issues.ts
2208
1800
  var DRAFT_ISSUE_SEVERITY = {
2209
1801
  // Every registered type — reported by validateDraft itself
@@ -2365,7 +1957,7 @@ function issue(code, path, message) {
2365
1957
  function isRecord(value) {
2366
1958
  return typeof value === "object" && value !== null && !Array.isArray(value);
2367
1959
  }
2368
- function valueAt(root, path) {
1960
+ function valueAt2(root, path) {
2369
1961
  let node = root;
2370
1962
  for (const segment of path) {
2371
1963
  if (typeof node !== "object" || node === null) {
@@ -2377,9 +1969,9 @@ function valueAt(root, path) {
2377
1969
  }
2378
1970
  function refusesEmpty(root, schemaIssue) {
2379
1971
  const { path } = schemaIssue;
2380
- const value = valueAt(root, path);
1972
+ const value = valueAt2(root, path);
2381
1973
  const index = path.at(-1);
2382
- const list = valueAt(root, path.slice(0, -1));
1974
+ const list = valueAt2(root, path.slice(0, -1));
2383
1975
  const empty = value === null || value === void 0 && typeof index === "number" && Array.isArray(list) && index < list.length;
2384
1976
  return empty && (schemaIssue.input === value || schemaIssue.code === "invalid_union");
2385
1977
  }
@@ -2906,6 +2498,70 @@ function checkTolerance(tolerance) {
2906
2498
  return issues;
2907
2499
  }
2908
2500
 
2501
+ // src/schemas/fill-in-the-blanks.ts
2502
+
2503
+ var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
2504
+ var TextMatchPolicySchema = _v4.z.looseObject({
2505
+ caseSensitive: _v4.z.boolean().optional(),
2506
+ trim: _v4.z.boolean().optional(),
2507
+ normalize: _v4.z.enum(["none", "NFC", "NFKC"]).optional(),
2508
+ foldDiacritics: _v4.z.boolean().optional(),
2509
+ collapseInnerWhitespace: _v4.z.boolean().optional(),
2510
+ ignorePunctuation: _v4.z.boolean().optional(),
2511
+ levenshtein: _v4.z.number().int().min(0).optional(),
2512
+ locale: _v4.z.string().optional()
2513
+ });
2514
+ var BlankConfigSchema = _v4.z.looseObject({
2515
+ id: _v4.z.string().min(1),
2516
+ acceptedAnswers: _v4.z.array(
2517
+ _v4.z.string().min(1).refine((answer) => answer.trim().length > 0, {
2518
+ error: "Accepted answers must contain non-whitespace characters."
2519
+ })
2520
+ ).min(1),
2521
+ caseSensitive: _v4.z.boolean().optional(),
2522
+ trimWhitespace: _v4.z.boolean().optional(),
2523
+ match: TextMatchPolicySchema.optional(),
2524
+ hint: _v4.z.string().optional(),
2525
+ feedback: _v4.z.string().optional()
2526
+ });
2527
+ var FillInTheBlanksDataSchema = _v4.z.looseObject({
2528
+ schemaVersion: _v4.z.literal("1.0"),
2529
+ type: _v4.z.literal("fill-in-the-blanks"),
2530
+ id: _v4.z.string().min(1),
2531
+ title: _v4.z.string().min(1),
2532
+ passage: _v4.z.string().min(1),
2533
+ passageHtml: _v4.z.string().optional(),
2534
+ blanks: _v4.z.array(BlankConfigSchema).min(1),
2535
+ scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
2536
+ media: MediaSchema.optional(),
2537
+ feedback: FeedbackSchema.optional(),
2538
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
2539
+ locale: _v4.z.string().optional(),
2540
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
2541
+ difficultyLevel: _v4.z.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, (_nullishCoalesce(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
+
2909
2565
  // src/authoring/fill-in-the-blanks.ts
2910
2566
  var fillInTheBlanksAuthoring = {
2911
2567
  createDraft: ({ newId }) => ({
@@ -3262,7 +2918,7 @@ function checkAnswerKey(gap, index, name, banks, bankIds) {
3262
2918
  if (typeof key !== "string") {
3263
2919
  return [];
3264
2920
  }
3265
- const choices = resolveChoices2(gap, banks, bankIds);
2921
+ const choices = resolveChoices(gap, banks, bankIds);
3266
2922
  if (choices === void 0) {
3267
2923
  return [];
3268
2924
  }
@@ -3275,7 +2931,7 @@ function checkAnswerKey(gap, index, name, banks, bankIds) {
3275
2931
  )
3276
2932
  ];
3277
2933
  }
3278
- function resolveChoices2(gap, banks, bankIds) {
2934
+ function resolveChoices(gap, banks, bankIds) {
3279
2935
  const hasOwn = !isUnset(gap.choices);
3280
2936
  const hasBank = !isUnwritten(gap.bankId);
3281
2937
  if (hasOwn === hasBank) {
@@ -3499,24 +3155,173 @@ function checkMultipleChoiceDraft(draft) {
3499
3155
  )
3500
3156
  );
3501
3157
  }
3502
- }
3503
- issues.push(...checkSharedOptional(draft));
3504
- return issues;
3505
- }
3506
- function checkOptionMedia(media, index, ordinal) {
3507
- const path = ["options", index, "media"];
3508
- const kind = media.type;
3509
- if (kind === "video" || kind === "embed") {
3510
- return [
3511
- issue(
3512
- "mc_option_media_kind",
3513
- [...path, "type"],
3514
- `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.`
3515
- )
3516
- ];
3517
- }
3518
- return checkMedia(media, path, MultipleChoiceOptionMediaSchema);
3519
- }
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
+
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 = _v4.z.strictObject({
3187
+ type: _v4.z.literal("audio"),
3188
+ url: MediaUrlSchema,
3189
+ alt: _v4.z.string().min(1).optional()
3190
+ });
3191
+ var ReferenceTextSchema = _v4.z.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 = _v4.z.looseObject({
3195
+ maxSeconds: _v4.z.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
3196
+ minSeconds: _v4.z.number().min(0).optional(),
3197
+ maxTakes: _v4.z.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
3198
+ });
3199
+ var ReadAloudDimensionWeightSchema = _v4.z.looseObject({
3200
+ name: _v4.z.enum(READ_ALOUD_DIMENSIONS),
3201
+ weight: _v4.z.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
3202
+ });
3203
+ var ReadAloudScoringSchema = _v4.z.looseObject({
3204
+ dimensions: _v4.z.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
3205
+ });
3206
+ var ReadAloudDataShape = _v4.z.looseObject({
3207
+ schemaVersion: _v4.z.literal("1.0"),
3208
+ type: _v4.z.literal("read-aloud"),
3209
+ id: _v4.z.string().min(1),
3210
+ title: _v4.z.string().min(1),
3211
+ instructions: _v4.z.string().optional(),
3212
+ referenceText: ReferenceTextSchema,
3213
+ locale: _v4.z.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: _v4.z.number().min(0).max(1).optional(),
3221
+ feedback: FeedbackSchema.optional(),
3222
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
3223
+ difficultyLevel: _v4.z.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) => (_nullishCoalesce(_optionalChain([issue2, 'access', _58 => _58.path, 'optionalAccess', _59 => _59.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 (_optionalChain([data, 'access', _60 => _60.media, 'optionalAccess', _61 => _61.playback, 'optionalAccess', _62 => _62.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 _optionalChain([data, 'access', _63 => _63.media, 'optionalAccess', _64 => _64.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
+ );
3520
3325
 
3521
3326
  // src/authoring/read-aloud.ts
3522
3327
  var readAloudAuthoring = {
@@ -3723,222 +3528,546 @@ function checkDimensions(scoring) {
3723
3528
  )
3724
3529
  );
3725
3530
  }
3726
- return issues;
3727
- }
3728
- function dimensionsRequired() {
3729
- return issue(
3730
- "ra_dimensions_required",
3731
- ["scoring", "dimensions"],
3732
- "Choose what the grade measures: at least one of accuracy, fluency, completeness or prosody, with a weight."
3733
- );
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
+
3746
+ var GapSelectChoiceSchema = _v4.z.looseObject({
3747
+ id: _v4.z.string().min(1),
3748
+ text: _v4.z.string().min(1)
3749
+ });
3750
+ var GapSelectBankSchema = _v4.z.looseObject({
3751
+ id: _v4.z.string().min(1),
3752
+ choices: _v4.z.array(GapSelectChoiceSchema).min(2)
3753
+ });
3754
+ var GapSelectGapSchema = _v4.z.looseObject({
3755
+ id: _v4.z.string().min(1),
3756
+ choices: _v4.z.array(GapSelectChoiceSchema).min(2).optional(),
3757
+ bankId: _v4.z.string().min(1).optional(),
3758
+ correctChoiceId: _v4.z.string().min(1),
3759
+ feedback: _v4.z.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 _optionalChain([banks, 'access', _65 => _65.find, 'call', _66 => _66((bank) => bank.id === gap.bankId), 'optionalAccess', _67 => _67.choices]);
3734
3769
  }
3735
- function checkModelRecording(media) {
3736
- if (!isRecord(media)) {
3737
- return [];
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, (_nullishCoalesce(counts.get(id), () => ( 0))) + 1);
3738
3775
  }
3739
- const kind = media.type;
3740
- return isUnwritten(kind) || kind === "audio" ? [] : [
3741
- issue(
3742
- "ra_media_kind",
3743
- ["media", "type"],
3744
- "A read-aloud model is read aloud: its recording must be audio."
3745
- )
3746
- ];
3776
+ return counts;
3747
3777
  }
3748
- function checkSlowRecording2(draft) {
3749
- const slow = draft.slowMedia;
3750
- if (!isRecord(slow)) {
3751
- return [];
3778
+ var GapSelectDataSchema = _v4.z.looseObject({
3779
+ schemaVersion: _v4.z.literal("1.0"),
3780
+ type: _v4.z.literal("gap-select"),
3781
+ id: _v4.z.string().min(1),
3782
+ title: _v4.z.string().min(1),
3783
+ passage: _v4.z.string().min(1),
3784
+ passageHtml: _v4.z.string().optional(),
3785
+ gaps: _v4.z.array(GapSelectGapSchema).min(1),
3786
+ banks: _v4.z.array(GapSelectBankSchema).optional(),
3787
+ scoringStrategy: _v4.z.enum(["all-or-nothing", "partial"]),
3788
+ presentation: _v4.z.literal("dropdown").optional(),
3789
+ shuffleChoices: _v4.z.boolean().optional(),
3790
+ media: MediaSchema.optional(),
3791
+ feedback: FeedbackSchema.optional(),
3792
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
3793
+ locale: _v4.z.string().optional(),
3794
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
3795
+ difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
3796
+ ai: AiPermissionsSchema.optional()
3797
+ }).refine(
3798
+ (data) => {
3799
+ const ids = (_nullishCoalesce(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"]
3752
3818
  }
3753
- const kind = slow.type;
3754
- const wrongKind = !isUnwritten(kind) && kind !== "audio";
3755
- const issues = wrongKind ? [issue("media_invalid", ["slowMedia", "type"], "The slow model recording must be audio.")] : [];
3756
- issues.push(
3757
- ...checkMedia(
3758
- wrongKind ? { ...slow, type: "audio" } : slow,
3759
- ["slowMedia"],
3760
- ReadAloudSlowMediaSchema
3761
- )
3762
- );
3763
- const media = draft.media;
3764
- if (isUnset(media)) {
3765
- issues.push(
3766
- issue(
3767
- "ra_slow_media_without_media",
3768
- ["media"],
3769
- "A slow model recording accompanies a recording. Add the recording first."
3770
- )
3771
- );
3772
- return issues;
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"]
3773
3824
  }
3774
- if (!isRecord(media)) {
3775
- return issues;
3825
+ ).refine(
3826
+ (data) => {
3827
+ const bankIds = new Set((_nullishCoalesce(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 = _nullishCoalesce(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
+
3851
+
3852
+ // src/schemas/written-response.ts
3853
+
3854
+ var WrittenResponseRubricCriterionSchema = _v4.z.looseObject({
3855
+ name: _v4.z.string().min(1),
3856
+ description: _v4.z.string().optional(),
3857
+ weight: _v4.z.number().min(0)
3858
+ });
3859
+ var WrittenResponseRubricSchema = _v4.z.looseObject({
3860
+ label: _v4.z.string().optional(),
3861
+ criteria: _v4.z.array(WrittenResponseRubricCriterionSchema).min(1)
3862
+ });
3863
+ var RedactedWrittenResponseRubricCriterionSchema = _v4.z.strictObject({
3864
+ name: _v4.z.string().min(1),
3865
+ description: _v4.z.string().optional(),
3866
+ weight: _v4.z.number().min(0)
3867
+ });
3868
+ var RedactedWrittenResponseRubricSchema = _v4.z.strictObject({
3869
+ label: _v4.z.string().optional(),
3870
+ criteria: _v4.z.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
3871
+ });
3872
+ var WrittenResponseDataSchema = _v4.z.looseObject({
3873
+ schemaVersion: _v4.z.literal("1.0"),
3874
+ type: _v4.z.literal("written-response"),
3875
+ id: _v4.z.string().min(1),
3876
+ title: _v4.z.string().min(1),
3877
+ prompt: _v4.z.string(),
3878
+ promptHtml: _v4.z.string().optional(),
3879
+ minWords: _v4.z.number().int().min(0),
3880
+ maxWords: _v4.z.number().int().min(1),
3881
+ rubric: WrittenResponseRubricSchema.optional(),
3882
+ languageTarget: _v4.z.string().optional(),
3883
+ media: MediaSchema.optional(),
3884
+ feedback: FeedbackSchema.optional(),
3885
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
3886
+ locale: _v4.z.string().optional(),
3887
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
3888
+ difficultyLevel: _v4.z.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: _v4.z.literal(true),
3899
+ /** Slot identity, carried through redaction so the client and the plan agree. */
3900
+ slotKey: _v4.z.string().min(1).optional(),
3901
+ schemaVersion: _v4.z.literal("1.0"),
3902
+ id: _v4.z.string().min(1),
3903
+ title: _v4.z.string().min(1),
3904
+ media: RedactedMediaSchema.optional(),
3905
+ passThreshold: _v4.z.number().min(0).max(1).optional(),
3906
+ locale: _v4.z.string().optional(),
3907
+ learningObjectives: _v4.z.array(_v4.z.string()).optional(),
3908
+ difficultyLevel: _v4.z.literal([1, 2, 3, 4, 5]).optional(),
3909
+ ai: _v4.z.strictObject({ explanations: _v4.z.boolean().optional(), hints: _v4.z.boolean().optional() }).optional()
3910
+ };
3911
+ var RedactedMultipleChoiceOptionMediaSchema = _v4.z.strictObject({
3912
+ type: _v4.z.enum(["image", "audio"]),
3913
+ url: _v4.z.string().min(1),
3914
+ alt: _v4.z.string().min(1).optional(),
3915
+ captionsUrl: _v4.z.string().min(1).optional()
3916
+ });
3917
+ var RedactedMultipleChoiceOptionSchema = _v4.z.strictObject({
3918
+ id: _v4.z.string().min(1),
3919
+ text: _v4.z.string().min(1),
3920
+ media: RedactedMultipleChoiceOptionMediaSchema.optional()
3921
+ });
3922
+ var RedactedMultipleChoiceDataSchema = _v4.z.strictObject({
3923
+ ...redactedBase,
3924
+ type: _v4.z.literal("multiple-choice"),
3925
+ question: _v4.z.string().min(1),
3926
+ questionHtml: _v4.z.string().optional(),
3927
+ mode: _v4.z.enum(["single", "multi"]),
3928
+ options: _v4.z.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
3929
+ shuffle: _v4.z.boolean().optional()
3930
+ });
3931
+ var RedactedBlankConfigSchema = _v4.z.strictObject({
3932
+ id: _v4.z.string().min(1),
3933
+ hint: _v4.z.string().optional()
3934
+ });
3935
+ var RedactedFillInTheBlanksDataSchema = _v4.z.strictObject({
3936
+ ...redactedBase,
3937
+ type: _v4.z.literal("fill-in-the-blanks"),
3938
+ passage: _v4.z.string().min(1),
3939
+ passageHtml: _v4.z.string().optional(),
3940
+ blanks: _v4.z.array(RedactedBlankConfigSchema).min(1)
3941
+ });
3942
+ var RedactedGapSelectChoiceSchema = _v4.z.strictObject({
3943
+ id: _v4.z.string().min(1),
3944
+ text: _v4.z.string().min(1)
3945
+ });
3946
+ var RedactedGapSelectBankSchema = _v4.z.strictObject({
3947
+ id: _v4.z.string().min(1),
3948
+ choices: _v4.z.array(RedactedGapSelectChoiceSchema).min(2)
3949
+ });
3950
+ var RedactedGapSelectGapSchema = _v4.z.strictObject({
3951
+ id: _v4.z.string().min(1),
3952
+ choices: _v4.z.array(RedactedGapSelectChoiceSchema).min(2).optional(),
3953
+ bankId: _v4.z.string().min(1).optional()
3954
+ });
3955
+ var RedactedGapSelectDataSchema = _v4.z.strictObject({
3956
+ ...redactedBase,
3957
+ type: _v4.z.literal("gap-select"),
3958
+ passage: _v4.z.string().min(1),
3959
+ passageHtml: _v4.z.string().optional(),
3960
+ gaps: _v4.z.array(RedactedGapSelectGapSchema).min(1),
3961
+ banks: _v4.z.array(RedactedGapSelectBankSchema).optional(),
3962
+ presentation: _v4.z.literal("dropdown").optional(),
3963
+ shuffleChoices: _v4.z.boolean().optional()
3964
+ });
3965
+ var RedactedWrittenResponseDataSchema = _v4.z.strictObject({
3966
+ ...redactedBase,
3967
+ type: _v4.z.literal("written-response"),
3968
+ prompt: _v4.z.string(),
3969
+ promptHtml: _v4.z.string().optional(),
3970
+ minWords: _v4.z.number().int().min(0),
3971
+ maxWords: _v4.z.number().int().min(1),
3972
+ rubric: RedactedWrittenResponseRubricSchema.optional(),
3973
+ languageTarget: _v4.z.string().optional()
3974
+ });
3975
+ var RedactedDictationSlowMediaSchema = _v4.z.strictObject({
3976
+ type: _v4.z.literal("audio"),
3977
+ url: MediaUrlSchema,
3978
+ alt: _v4.z.string().min(1).optional()
3979
+ });
3980
+ var RedactedDictationDataSchema = _v4.z.strictObject({
3981
+ ...redactedBase,
3982
+ type: _v4.z.literal("dictation"),
3983
+ slowMedia: RedactedDictationSlowMediaSchema.optional(),
3984
+ hints: _v4.z.strictObject({ mode: _v4.z.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: _optionalChain([data, 'access', _68 => _68.media, 'optionalAccess', _69 => _69[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
+ });
3776
3994
  }
3777
- if (isRecord(media.playback) && media.playback.maxPlays !== void 0) {
3778
- issues.push(
3779
- issue(
3780
- "ra_slow_media_beside_play_limit",
3781
- ["slowMedia"],
3782
- "A play budget on the recording cannot coexist with a slow recording that has none. Remove the slow recording, or remove the play limit."
3783
- )
3784
- );
3995
+ if (data.slowMedia !== void 0 && _optionalChain([data, 'access', _70 => _70.media, 'optionalAccess', _71 => _71.playback, 'optionalAccess', _72 => _72.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
+ });
3785
4002
  }
3786
- if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) {
3787
- issues.push(
3788
- issue(
3789
- "ra_slow_media_same_recording",
3790
- ["slowMedia", "url"],
3791
- "The slow model recording is the same file as the recording. Point it at the slower reading."
3792
- )
3793
- );
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
+ });
3794
4010
  }
3795
- return issues;
3796
- }
3797
-
3798
- // src/authoring/written-response.ts
3799
- var writtenResponseAuthoring = {
3800
- createDraft: ({ newId }) => ({
3801
- schemaVersion: "1.0",
3802
- type: "written-response",
3803
- id: newId(),
3804
- title: "",
3805
- prompt: "",
3806
- minWords: 0,
3807
- maxWords: 0
4011
+ });
4012
+ var RedactedReadAloudDataSchema = _v4.z.strictObject({
4013
+ ...redactedBase,
4014
+ type: _v4.z.literal("read-aloud"),
4015
+ locale: _v4.z.string().regex(CANONICAL_LOCALE_RE),
4016
+ instructions: _v4.z.string().optional(),
4017
+ referenceText: _v4.z.string().min(1),
4018
+ slowMedia: ReadAloudSlowMediaSchema.optional(),
4019
+ recording: _v4.z.strictObject({
4020
+ maxSeconds: _v4.z.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
4021
+ minSeconds: _v4.z.number().min(0).optional(),
4022
+ maxTakes: _v4.z.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
3808
4023
  }),
3809
- checkDraft: checkWrittenResponseDraft
3810
- };
3811
- function checkWrittenResponseDraft(draft) {
3812
- const issues = checkIdentity(draft, "written-response");
3813
- if (isUnwritten(draft.prompt)) {
3814
- issues.push(
3815
- issue(
3816
- "wr_prompt_required",
3817
- ["prompt"],
3818
- 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."
3819
- )
3820
- );
3821
- }
3822
- const min = draft.minWords;
3823
- const max = draft.maxWords;
3824
- if (isUnset(min)) {
3825
- issues.push(
3826
- issue(
3827
- "wr_min_words_required",
3828
- ["minWords"],
3829
- "Set the minimum number of words. Use 0 for no minimum."
3830
- )
3831
- );
3832
- } else if (!(isWholeNumber(min) && min >= 0)) {
3833
- issues.push(
3834
- issue(
3835
- "wr_min_words_invalid",
3836
- ["minWords"],
3837
- isTooLarge(min) ? "The minimum word count is too large." : "The minimum word count must be a whole number, 0 or more."
3838
- )
3839
- );
3840
- }
3841
- if (isUnset(max) || max === 0) {
3842
- issues.push(issue("wr_max_words_required", ["maxWords"], "Set the maximum number of words."));
3843
- } else if (!(isWholeNumber(max) && max >= 1)) {
3844
- issues.push(
3845
- issue(
3846
- "wr_max_words_invalid",
3847
- ["maxWords"],
3848
- isTooLarge(max) ? "The maximum word count is too large." : "The maximum word count must be a whole number, 1 or more."
3849
- )
3850
- );
3851
- } else if (typeof min === "number" && max < min) {
3852
- issues.push(
3853
- issue(
3854
- "wr_word_bounds_order",
3855
- ["maxWords"],
3856
- "The maximum word count cannot be lower than the minimum."
3857
- )
3858
- );
3859
- }
3860
- const rubric = draft.rubric;
3861
- if (isRecord(rubric)) {
3862
- const criteria = isUnset(rubric.criteria) ? [] : rubric.criteria;
3863
- if (Array.isArray(criteria)) {
3864
- issues.push(...checkCriteria(criteria));
3865
- }
4024
+ scoring: _v4.z.strictObject({
4025
+ dimensions: _v4.z.array(
4026
+ _v4.z.strictObject({
4027
+ name: _v4.z.enum(READ_ALOUD_DIMENSIONS),
4028
+ weight: _v4.z.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
+ });
3866
4041
  }
3867
- issues.push(...checkSharedOptional(draft));
3868
- return issues;
3869
- }
3870
- function checkCriteria(criteria) {
3871
- const issues = [];
3872
- if (criteria.length === 0) {
3873
- issues.push(
3874
- issue(
3875
- "wr_rubric_criteria_required",
3876
- ["rubric", "criteria"],
3877
- "Add at least one rubric criterion, or remove the rubric."
3878
- )
3879
- );
3880
- return issues;
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
+ });
3881
4049
  }
3882
- let totalWeight = 0;
3883
- let everyWeightUsable = true;
3884
- for (const [index, criterion] of criteria.entries()) {
3885
- if (!isRecord(criterion)) {
3886
- everyWeightUsable = false;
3887
- continue;
3888
- }
3889
- const ordinal = index + 1;
3890
- const path = ["rubric", "criteria", index];
3891
- if (isUnwritten(criterion.name)) {
3892
- issues.push(
3893
- issue("wr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`)
3894
- );
3895
- }
3896
- const weight = criterion.weight;
3897
- if (isUnset(weight)) {
3898
- everyWeightUsable = false;
3899
- issues.push(
3900
- issue(
3901
- "wr_criterion_weight_required",
3902
- [...path, "weight"],
3903
- `Give rubric criterion ${ordinal} a weight.`
3904
- )
3905
- );
3906
- } else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
3907
- everyWeightUsable = false;
3908
- issues.push(
3909
- issue(
3910
- "wr_criterion_weight_invalid",
3911
- [...path, "weight"],
3912
- "A rubric weight must be a number, 0 or more."
3913
- )
3914
- );
3915
- } else {
3916
- totalWeight += weight;
3917
- }
4050
+ if (data.slowMedia !== void 0 && _optionalChain([data, 'access', _73 => _73.media, 'optionalAccess', _74 => _74.playback, 'optionalAccess', _75 => _75.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
+ });
3918
4057
  }
3919
- if (everyWeightUsable && totalWeight === 0) {
3920
- issues.push(
3921
- issue(
3922
- "wr_rubric_weights_zero",
3923
- ["rubric", "criteria"],
3924
- "Give at least one rubric criterion a weight above 0. With every weight at 0, no weighted total can be computed."
3925
- )
3926
- );
3927
- } else if (totalWeight === Number.POSITIVE_INFINITY) {
3928
- issues.push(
3929
- issue(
3930
- "wr_rubric_weights_too_large",
3931
- ["rubric", "criteria"],
3932
- "The rubric weights add up to more than can be calculated with. Use smaller weights."
3933
- )
3934
- );
4058
+ if (data.slowMedia !== void 0 && data.slowMedia.url === _optionalChain([data, 'access', _76 => _76.media, 'optionalAccess', _77 => _77.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
+ });
3935
4065
  }
3936
- return issues;
3937
- }
4066
+ });
3938
4067
 
3939
4068
  // src/scoring/activity-scorers/dictation.ts
3940
4069
  function scoreDictation(data, response) {
3941
- const alignment = alignDictation(data, typeof _optionalChain([response, 'optionalAccess', _75 => _75.text]) === "string" ? response.text : "");
4070
+ const alignment = alignDictation(data, typeof _optionalChain([response, 'optionalAccess', _78 => _78.text]) === "string" ? response.text : "");
3942
4071
  const details = [];
3943
4072
  for (const word of alignment.words) {
3944
4073
  if (word.status === "extra") {
@@ -3946,7 +4075,6 @@ function scoreDictation(data, response) {
3946
4075
  }
3947
4076
  details.push({
3948
4077
  itemId: word.itemId,
3949
- correct: word.status === "correct",
3950
4078
  outcome: word.status === "correct" ? "correct" : word.status === "incorrect" ? "incorrect" : "incorrect-omission",
3951
4079
  learnerResponse: word.attempt,
3952
4080
  correctResponse: word.reference,
@@ -3990,7 +4118,6 @@ function scoreFillInTheBlanks(data, response) {
3990
4118
  perBlankCorrect.push(matched);
3991
4119
  details.push({
3992
4120
  itemId: blank.id,
3993
- correct: matched,
3994
4121
  outcome: matched ? "correct" : "incorrect",
3995
4122
  learnerResponse: [input],
3996
4123
  correctResponse: [...blank.acceptedAnswers],
@@ -4007,7 +4134,7 @@ function choicesFor(data, gap) {
4007
4134
  if (gap.choices !== void 0) {
4008
4135
  return gap.choices;
4009
4136
  }
4010
- return _nullishCoalesce(_optionalChain([data, 'access', _76 => _76.banks, 'optionalAccess', _77 => _77.find, 'call', _78 => _78((bank) => bank.id === gap.bankId), 'optionalAccess', _79 => _79.choices]), () => ( []));
4137
+ return _nullishCoalesce(_optionalChain([data, 'access', _79 => _79.banks, 'optionalAccess', _80 => _80.find, 'call', _81 => _81((bank) => bank.id === gap.bankId), 'optionalAccess', _82 => _82.choices]), () => ( []));
4011
4138
  }
4012
4139
  function scoreGapSelect(data, response) {
4013
4140
  const details = [];
@@ -4022,7 +4149,6 @@ function scoreGapSelect(data, response) {
4022
4149
  perGapCorrect.push(correct);
4023
4150
  details.push({
4024
4151
  itemId: gap.id,
4025
- correct,
4026
4152
  outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
4027
4153
  learnerResponse: [selected],
4028
4154
  correctResponse: [gap.correctChoiceId],
@@ -4043,7 +4169,7 @@ function scoreMultipleChoice(data, response) {
4043
4169
  let scoreValue;
4044
4170
  if (data.scoringStrategy === "all-or-nothing") {
4045
4171
  if (data.mode === "single") {
4046
- scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _80 => _80.get, 'call', _81 => _81(response.selectedOptionIds[0]), 'optionalAccess', _82 => _82.isCorrect]) === true ? 1 : 0;
4172
+ scoreValue = response.selectedOptionIds.length === 1 && _optionalChain([optionById, 'access', _83 => _83.get, 'call', _84 => _84(response.selectedOptionIds[0]), 'optionalAccess', _85 => _85.isCorrect]) === true ? 1 : 0;
4047
4173
  } else {
4048
4174
  const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
4049
4175
  const allCorrectSelected = correctIds.every((id) => selected.has(id));
@@ -4054,7 +4180,7 @@ function scoreMultipleChoice(data, response) {
4054
4180
  let incorrectSelected = 0;
4055
4181
  for (const id of selected) {
4056
4182
  const option = optionById.get(id);
4057
- if (_optionalChain([option, 'optionalAccess', _83 => _83.isCorrect])) {
4183
+ if (_optionalChain([option, 'optionalAccess', _86 => _86.isCorrect])) {
4058
4184
  correctSelected += 1;
4059
4185
  } else {
4060
4186
  incorrectSelected += 1;
@@ -4067,7 +4193,6 @@ function scoreMultipleChoice(data, response) {
4067
4193
  const outcome = wasSelected ? option.isCorrect ? "correct" : "incorrect" : option.isCorrect ? "incorrect-omission" : "correct-omission";
4068
4194
  return {
4069
4195
  itemId: option.id,
4070
- correct: wasSelected === option.isCorrect,
4071
4196
  outcome,
4072
4197
  learnerResponse: [wasSelected ? "selected" : "not-selected"],
4073
4198
  correctResponse: [option.isCorrect ? "selected" : "not-selected"],
@@ -4238,7 +4363,7 @@ var multipleChoiceType = defineActivityType({
4238
4363
  // exact optionals. Structurally identical at runtime — cast is type-level only.
4239
4364
  schema: MultipleChoiceDataSchema,
4240
4365
  scoring: { kind: "sync", score: scoreMultipleChoice },
4241
- isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _84 => _84.selectedOptionIds, 'access', _85 => _85.length]), () => ( 0))) > 0,
4366
+ isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _87 => _87.selectedOptionIds, 'access', _88 => _88.length]), () => ( 0))) > 0,
4242
4367
  fieldPolicy: MULTIPLE_CHOICE_FIELD_POLICY,
4243
4368
  redactedSchema: RedactedMultipleChoiceDataSchema,
4244
4369
  interop: {
@@ -4255,7 +4380,7 @@ var fillInTheBlanksType = defineActivityType({
4255
4380
  type: "fill-in-the-blanks",
4256
4381
  schema: FillInTheBlanksDataSchema,
4257
4382
  scoring: { kind: "sync", score: scoreFillInTheBlanks },
4258
- isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _86 => _86.answers]), () => ( {}))).some((answer) => answer.trim().length > 0),
4383
+ isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _89 => _89.answers]), () => ( {}))).some((answer) => answer.trim().length > 0),
4259
4384
  fieldPolicy: FILL_IN_THE_BLANKS_FIELD_POLICY,
4260
4385
  redactedSchema: RedactedFillInTheBlanksDataSchema,
4261
4386
  interop: {
@@ -4278,14 +4403,14 @@ var writtenResponseType = defineActivityType({
4278
4403
  kind: "deferred",
4279
4404
  reason: "requires_async_grading",
4280
4405
  partial: (data, response) => {
4281
- const wordCount = countWords(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _87 => _87.text]), () => ( "")));
4406
+ const wordCount = countWords(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _90 => _90.text]), () => ( "")));
4282
4407
  return {
4283
4408
  withinWordBounds: response !== void 0 && wordCount >= data.minWords && wordCount <= data.maxWords,
4284
4409
  wordCount
4285
4410
  };
4286
4411
  }
4287
4412
  },
4288
- isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _88 => _88.text, 'access', _89 => _89.trim, 'call', _90 => _90(), 'access', _91 => _91.length]), () => ( 0))) > 0,
4413
+ isAnswered: (response) => (_nullishCoalesce(_optionalChain([response, 'optionalAccess', _91 => _91.text, 'access', _92 => _92.trim, 'call', _93 => _93(), 'access', _94 => _94.length]), () => ( 0))) > 0,
4289
4414
  fieldPolicy: WRITTEN_RESPONSE_FIELD_POLICY,
4290
4415
  redactedSchema: RedactedWrittenResponseDataSchema,
4291
4416
  interop: {
@@ -4300,7 +4425,7 @@ var gapSelectType = defineActivityType({
4300
4425
  type: "gap-select",
4301
4426
  schema: GapSelectDataSchema,
4302
4427
  scoring: { kind: "sync", score: scoreGapSelect },
4303
- isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _92 => _92.selections]), () => ( {}))).some((choiceId) => choiceId !== ""),
4428
+ isAnswered: (response) => Object.values(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _95 => _95.selections]), () => ( {}))).some((choiceId) => choiceId !== ""),
4304
4429
  fieldPolicy: GAP_SELECT_FIELD_POLICY,
4305
4430
  redactedSchema: RedactedGapSelectDataSchema,
4306
4431
  interop: {
@@ -4331,7 +4456,7 @@ var dictationType = defineActivityType({
4331
4456
  // cut where the scorer cuts, so text past the cap never counts as an answer.
4332
4457
  isAnswered: (response) => normalizeDictationText(
4333
4458
  truncateCodePoints(
4334
- typeof _optionalChain([response, 'optionalAccess', _93 => _93.text]) === "string" ? response.text : "",
4459
+ typeof _optionalChain([response, 'optionalAccess', _96 => _96.text]) === "string" ? response.text : "",
4335
4460
  DICTATION_MAX_TEXT_LENGTH
4336
4461
  )
4337
4462
  ) !== "",
@@ -4357,7 +4482,7 @@ var READ_ALOUD_FIELD_POLICY = {
4357
4482
  feedback: FEEDBACK_FIELD_POLICY
4358
4483
  };
4359
4484
  function hasRecording(response) {
4360
- const recording = _optionalChain([response, 'optionalAccess', _94 => _94.recording]);
4485
+ const recording = _optionalChain([response, 'optionalAccess', _97 => _97.recording]);
4361
4486
  return typeof recording === "object" && recording !== null && typeof recording.key === "string" && recording.key !== "";
4362
4487
  }
4363
4488
  var readAloudType = defineActivityType({
@@ -4383,6 +4508,19 @@ var readAloudType = defineActivityType({
4383
4508
  interactions: ["recording-started", "recording-stopped", "recording-uploaded", "submitted"],
4384
4509
  authoring: readAloudAuthoring
4385
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
+ }
4386
4524
  registerActivityType(multipleChoiceType);
4387
4525
  registerActivityType(fillInTheBlanksType);
4388
4526
  registerActivityType(writtenResponseType);
@@ -4455,32 +4593,5 @@ registerActivityType(readAloudType);
4455
4593
 
4456
4594
 
4457
4595
 
4458
-
4459
-
4460
-
4461
-
4462
-
4463
-
4464
-
4465
-
4466
-
4467
-
4468
-
4469
-
4470
-
4471
-
4472
-
4473
-
4474
-
4475
-
4476
-
4477
-
4478
-
4479
-
4480
-
4481
-
4482
-
4483
-
4484
-
4485
- exports.AiPermissionsSchema = AiPermissionsSchema; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.NativeControlHintSchema = NativeControlHintSchema; exports.MediaPlaybackSchema = MediaPlaybackSchema; exports.MediaTrackSchema = MediaTrackSchema; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.PLACEHOLDER_RE = PLACEHOLDER_RE; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.withValidationScope = withValidationScope; exports.DICTATION_MAX_TRANSCRIPT_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH; exports.DICTATION_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH; exports.DICTATION_MAX_EQUIVALENCE_LENGTH = DICTATION_MAX_EQUIVALENCE_LENGTH; exports.DICTATION_MAX_EQUIVALENCES = DICTATION_MAX_EQUIVALENCES; exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = DICTATION_MAX_ACCEPTED_TRANSCRIPTS; exports.codePointLength = codePointLength; exports.normalizeDictationText = normalizeDictationText; exports.dictationReferenceWords = dictationReferenceWords; exports.alignDictation = alignDictation; exports.STEP = STEP; exports.alignSequences = alignSequences; exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.DictationSlowMediaSchema = DictationSlowMediaSchema; exports.DictationEquivalenceSchema = DictationEquivalenceSchema; exports.DictationToleranceSchema = DictationToleranceSchema; exports.DictationDataSchema = DictationDataSchema; exports.groupCaptionsField = groupCaptionsField; exports.GROUP_CAPTIONS_REVEAL_DICTATION = GROUP_CAPTIONS_REVEAL_DICTATION; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.isUnset = isUnset; exports.isUnwritten = isUnwritten; exports.isMissingId = isMissingId; exports.checkMedia = checkMedia; exports.covers = covers; exports.pathKey = pathKey; exports.coveredPathsOf = coveredPathsOf; exports.MultipleChoiceOptionMediaSchema = MultipleChoiceOptionMediaSchema; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.READ_ALOUD_MAX_REFERENCE_LENGTH = READ_ALOUD_MAX_REFERENCE_LENGTH; exports.READ_ALOUD_MAX_SECONDS = READ_ALOUD_MAX_SECONDS; exports.READ_ALOUD_MAX_TAKES = READ_ALOUD_MAX_TAKES; exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = READ_ALOUD_MAX_DIMENSION_WEIGHT; exports.SPEECH_ASSESSMENT_MAX_WORDS = SPEECH_ASSESSMENT_MAX_WORDS; exports.SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = SPEECH_ASSESSMENT_MAX_TEXT_LENGTH; exports.CANONICAL_LOCALE_RE = CANONICAL_LOCALE_RE; exports.ReadAloudSlowMediaSchema = ReadAloudSlowMediaSchema; exports.ReadAloudDataSchema = ReadAloudDataSchema; exports.countWords = countWords; exports.GapSelectChoiceSchema = GapSelectChoiceSchema; exports.GapSelectBankSchema = GapSelectBankSchema; exports.GapSelectGapSchema = GapSelectGapSchema; exports.GapSelectDataSchema = GapSelectDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionMediaSchema = RedactedMultipleChoiceOptionMediaSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedGapSelectChoiceSchema = RedactedGapSelectChoiceSchema; exports.RedactedGapSelectBankSchema = RedactedGapSelectBankSchema; exports.RedactedGapSelectGapSchema = RedactedGapSelectGapSchema; exports.RedactedGapSelectDataSchema = RedactedGapSelectDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.RedactedDictationSlowMediaSchema = RedactedDictationSlowMediaSchema; exports.RedactedDictationDataSchema = RedactedDictationDataSchema; exports.RedactedReadAloudDataSchema = RedactedReadAloudDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType; exports.gapSelectType = gapSelectType; exports.dictationType = dictationType; exports.readAloudType = readAloudType;
4486
- //# sourceMappingURL=chunk-D4TOPSHV.cjs.map
4596
+ exports.maxUnits = maxUnits; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.PLACEHOLDER_RE = PLACEHOLDER_RE; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.withValidationScope = withValidationScope; exports.DICTATION_MAX_TRANSCRIPT_LENGTH = DICTATION_MAX_TRANSCRIPT_LENGTH; exports.DICTATION_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH; exports.DICTATION_MAX_EQUIVALENCE_LENGTH = DICTATION_MAX_EQUIVALENCE_LENGTH; exports.DICTATION_MAX_EQUIVALENCES = DICTATION_MAX_EQUIVALENCES; exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = DICTATION_MAX_ACCEPTED_TRANSCRIPTS; exports.codePointLength = codePointLength; exports.normalizeDictationText = normalizeDictationText; exports.dictationReferenceWords = dictationReferenceWords; exports.alignDictation = alignDictation; exports.STEP = STEP; exports.alignSequences = alignSequences; exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.DictationDataSchema = DictationDataSchema; exports.groupCaptionsField = groupCaptionsField; exports.GROUP_CAPTIONS_REVEAL_DICTATION = GROUP_CAPTIONS_REVEAL_DICTATION; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.isUnset = isUnset; exports.isUnwritten = isUnwritten; exports.isMissingId = isMissingId; exports.checkMedia = checkMedia; exports.covers = covers; exports.pathKey = pathKey; exports.coveredPathsOf = coveredPathsOf; exports.MultipleChoiceOptionMediaSchema = MultipleChoiceOptionMediaSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.READ_ALOUD_MAX_REFERENCE_LENGTH = READ_ALOUD_MAX_REFERENCE_LENGTH; exports.READ_ALOUD_MAX_SECONDS = READ_ALOUD_MAX_SECONDS; exports.READ_ALOUD_MAX_TAKES = READ_ALOUD_MAX_TAKES; exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = READ_ALOUD_MAX_DIMENSION_WEIGHT; exports.SPEECH_ASSESSMENT_MAX_WORDS = SPEECH_ASSESSMENT_MAX_WORDS; exports.SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = SPEECH_ASSESSMENT_MAX_TEXT_LENGTH; exports.CANONICAL_LOCALE_RE = CANONICAL_LOCALE_RE; exports.ReadAloudDataSchema = ReadAloudDataSchema; exports.countWords = countWords; exports.GapSelectDataSchema = GapSelectDataSchema; exports.isOwnSchema = isOwnSchema; exports.readSchema = readSchema; exports.standardJsonSchemaOf = standardJsonSchemaOf; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType; exports.gapSelectType = gapSelectType; exports.dictationType = dictationType; exports.readAloudType = readAloudType;
4597
+ //# sourceMappingURL=chunk-DSDXL3JO.cjs.map