@intellectif/lk-core 0.22.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.
- package/CHANGELOG.md +15 -0
- package/README.md +5 -5
- package/dist/{activity-CYc7SD3d.d.cts → activity-DgpBJeBc.d.cts} +11 -15
- package/dist/{activity-CYc7SD3d.d.ts → activity-DgpBJeBc.d.ts} +11 -15
- package/dist/ai-check.cjs +8 -8
- package/dist/ai-check.d.cts +1 -1
- package/dist/ai-check.d.ts +1 -1
- package/dist/ai-check.js +3 -3
- package/dist/{chunk-2CV6NU32.cjs → chunk-2JT2WUQ2.cjs} +77 -39
- package/dist/chunk-2JT2WUQ2.cjs.map +1 -0
- package/dist/{chunk-D4TOPSHV.cjs → chunk-DSDXL3JO.cjs} +1216 -1105
- package/dist/chunk-DSDXL3JO.cjs.map +1 -0
- package/dist/{chunk-D3GP5MET.js → chunk-FBKTION6.js} +1093 -982
- package/dist/chunk-FBKTION6.js.map +1 -0
- package/dist/{chunk-Q7MV3IXJ.js → chunk-IARLJ5MW.js} +15 -16
- package/dist/chunk-IARLJ5MW.js.map +1 -0
- package/dist/{chunk-5UCR4UNR.js → chunk-RCZCP5OE.js} +12 -7
- package/dist/chunk-RCZCP5OE.js.map +1 -0
- package/dist/{chunk-X5DAXBJI.js → chunk-SJVRUPJT.js} +2 -2
- package/dist/{chunk-EOFWSGLU.cjs → chunk-TJJFKKH2.cjs} +4 -4
- package/dist/{chunk-EOFWSGLU.cjs.map → chunk-TJJFKKH2.cjs.map} +1 -1
- package/dist/{chunk-WQVZPIN7.cjs → chunk-VSH7ECQL.cjs} +44 -45
- package/dist/chunk-VSH7ECQL.cjs.map +1 -0
- package/dist/{chunk-OPMY2MNS.cjs → chunk-W5KB3DNT.cjs} +19 -14
- package/dist/chunk-W5KB3DNT.cjs.map +1 -0
- package/dist/{chunk-Z7QQZ53L.js → chunk-XS6U2ARZ.js} +70 -32
- package/dist/chunk-XS6U2ARZ.js.map +1 -0
- package/dist/index-BVNeSCWC.d.ts +548 -0
- package/dist/index-BlOLlwOi.d.cts +548 -0
- package/dist/index.cjs +129 -206
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -15
- package/dist/index.d.ts +115 -15
- package/dist/index.js +26 -103
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +3 -94
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -3
- package/dist/schemas.d.ts +2 -3
- package/dist/schemas.js +6 -97
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +4 -4
- package/vectors/README.md +32 -2
- package/vectors/scoring.json +1 -498
- package/vectors/validation.json +6742 -0
- package/vectors/validation.mjs +195 -0
- package/dist/chunk-2CV6NU32.cjs.map +0 -1
- package/dist/chunk-5UCR4UNR.js.map +0 -1
- package/dist/chunk-D3GP5MET.js.map +0 -1
- package/dist/chunk-D4TOPSHV.cjs.map +0 -1
- package/dist/chunk-OPMY2MNS.cjs.map +0 -1
- package/dist/chunk-Q7MV3IXJ.js.map +0 -1
- package/dist/chunk-WQVZPIN7.cjs.map +0 -1
- package/dist/chunk-Z7QQZ53L.js.map +0 -1
- package/dist/index-0Z3-nnAP.d.ts +0 -2303
- package/dist/index-CxxiNSSH.d.cts +0 -2303
- /package/dist/{chunk-X5DAXBJI.js.map → chunk-SJVRUPJT.js.map} +0 -0
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
// src/schemas/
|
|
1
|
+
// src/schemas/media.ts
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
var AiPermissionsSchema = z.looseObject({
|
|
4
|
-
explanations: z.boolean().optional(),
|
|
5
|
-
hints: z.boolean().optional()
|
|
6
|
-
});
|
|
7
3
|
|
|
8
|
-
// src/schemas/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
z3.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
20
|
+
var MediaUrlSchema = z.union([
|
|
21
|
+
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."
|
|
20
23
|
}),
|
|
21
|
-
|
|
24
|
+
z.string().regex(/^\/(?!\/)\S*$/, {
|
|
22
25
|
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
23
26
|
})
|
|
24
27
|
]);
|
|
25
|
-
var NativeControlHintSchema =
|
|
26
|
-
var MediaPlaybackSchema =
|
|
28
|
+
var NativeControlHintSchema = z.enum(["hide-download", "hide-rate"]);
|
|
29
|
+
var MediaPlaybackSchema = z.strictObject({
|
|
27
30
|
/**
|
|
28
31
|
* `native` (the resolved default) renders today's `<audio controls>`
|
|
29
32
|
* unchanged. `minimal` — resolved automatically whenever any enforcement
|
|
@@ -31,29 +34,31 @@ var MediaPlaybackSchema = z3.strictObject({
|
|
|
31
34
|
* play/pause, elapsed/total, mute, volume, optional speed, optional
|
|
32
35
|
* scrubber, and a live plays-remaining status.
|
|
33
36
|
*/
|
|
34
|
-
controls:
|
|
37
|
+
controls: z.enum(["native", "minimal"]).optional(),
|
|
35
38
|
/**
|
|
36
39
|
* How many times the recording may be STARTED. A play is consumed when
|
|
37
40
|
* playback begins from anywhere other than where it last stopped, so
|
|
38
41
|
* pausing, resuming, and paging between the questions of one listening
|
|
39
42
|
* group are all free. Enforced in `practice` and `exam`; never in `review`.
|
|
40
43
|
*/
|
|
41
|
-
maxPlays:
|
|
44
|
+
maxPlays: z.number().int().min(1).max(20).optional(),
|
|
42
45
|
/** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
|
|
43
|
-
seek:
|
|
46
|
+
seek: z.enum(["allow", "none"]).optional(),
|
|
44
47
|
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
45
|
-
rate:
|
|
48
|
+
rate: z.enum(["allow", "fixed"]).optional(),
|
|
46
49
|
/** Advisory only. See {@link NativeControlHintSchema}. */
|
|
47
|
-
nativeControlHints:
|
|
50
|
+
nativeControlHints: z.array(NativeControlHintSchema).min(1).max(2).optional()
|
|
48
51
|
});
|
|
49
|
-
var MediaTrackSchema =
|
|
50
|
-
kind:
|
|
52
|
+
var MediaTrackSchema = z.strictObject({
|
|
53
|
+
kind: z.enum(["captions", "subtitles"]),
|
|
51
54
|
src: MediaUrlSchema,
|
|
52
|
-
srclang:
|
|
55
|
+
srclang: 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:
|
|
56
|
-
|
|
58
|
+
label: maxUnits(z.string(), 60).refine((label) => label.trim().length > 0, {
|
|
59
|
+
error: "A track label must not be empty."
|
|
60
|
+
}),
|
|
61
|
+
default: z.boolean().optional()
|
|
57
62
|
});
|
|
58
63
|
var MAX_TRACKS = 12;
|
|
59
64
|
function posterFits(m) {
|
|
@@ -76,12 +81,12 @@ var TRACKS_DISTINCT = {
|
|
|
76
81
|
error: "tracks: at most one may be the default, and no two may share a kind and a language \u2014 a menu listing one language twice cannot say which one it plays.",
|
|
77
82
|
path: ["tracks"]
|
|
78
83
|
};
|
|
79
|
-
var MediaSchema =
|
|
80
|
-
type:
|
|
84
|
+
var MediaSchema = z.looseObject({
|
|
85
|
+
type: z.enum(["image", "audio", "video", "embed"]),
|
|
81
86
|
url: MediaUrlSchema,
|
|
82
|
-
alt:
|
|
87
|
+
alt: z.string().min(1).optional(),
|
|
83
88
|
captionsUrl: MediaUrlSchema.optional(),
|
|
84
|
-
tracks:
|
|
89
|
+
tracks: z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
85
90
|
poster: MediaUrlSchema.optional(),
|
|
86
91
|
playback: MediaPlaybackSchema.optional()
|
|
87
92
|
}).refine(
|
|
@@ -118,12 +123,12 @@ var MediaSchema = z3.looseObject({
|
|
|
118
123
|
path: ["playback", "nativeControlHints"]
|
|
119
124
|
}
|
|
120
125
|
);
|
|
121
|
-
var RedactedMediaSchema =
|
|
122
|
-
type:
|
|
126
|
+
var RedactedMediaSchema = z.strictObject({
|
|
127
|
+
type: z.enum(["image", "audio", "video", "embed"]),
|
|
123
128
|
url: MediaUrlSchema,
|
|
124
|
-
alt:
|
|
129
|
+
alt: z.string().min(1).optional(),
|
|
125
130
|
captionsUrl: MediaUrlSchema.optional(),
|
|
126
|
-
tracks:
|
|
131
|
+
tracks: z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
127
132
|
poster: MediaUrlSchema.optional(),
|
|
128
133
|
playback: MediaPlaybackSchema.optional()
|
|
129
134
|
}).refine(
|
|
@@ -137,70 +142,6 @@ var RedactedMediaSchema = z3.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
|
-
import { z as z4 } from "zod/v4";
|
|
142
|
-
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
143
|
-
var TextMatchPolicySchema = z4.looseObject({
|
|
144
|
-
caseSensitive: z4.boolean().optional(),
|
|
145
|
-
trim: z4.boolean().optional(),
|
|
146
|
-
normalize: z4.enum(["none", "NFC", "NFKC"]).optional(),
|
|
147
|
-
foldDiacritics: z4.boolean().optional(),
|
|
148
|
-
collapseInnerWhitespace: z4.boolean().optional(),
|
|
149
|
-
ignorePunctuation: z4.boolean().optional(),
|
|
150
|
-
levenshtein: z4.number().int().min(0).optional(),
|
|
151
|
-
locale: z4.string().optional()
|
|
152
|
-
});
|
|
153
|
-
var BlankConfigSchema = z4.looseObject({
|
|
154
|
-
id: z4.string().min(1),
|
|
155
|
-
acceptedAnswers: z4.array(
|
|
156
|
-
z4.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
157
|
-
error: "Accepted answers must contain non-whitespace characters."
|
|
158
|
-
})
|
|
159
|
-
).min(1),
|
|
160
|
-
caseSensitive: z4.boolean().optional(),
|
|
161
|
-
trimWhitespace: z4.boolean().optional(),
|
|
162
|
-
match: TextMatchPolicySchema.optional(),
|
|
163
|
-
hint: z4.string().optional(),
|
|
164
|
-
feedback: z4.string().optional()
|
|
165
|
-
});
|
|
166
|
-
var FillInTheBlanksDataSchema = z4.looseObject({
|
|
167
|
-
schemaVersion: z4.literal("1.0"),
|
|
168
|
-
type: z4.literal("fill-in-the-blanks"),
|
|
169
|
-
id: z4.string().min(1),
|
|
170
|
-
title: z4.string().min(1),
|
|
171
|
-
passage: z4.string().min(1),
|
|
172
|
-
passageHtml: z4.string().optional(),
|
|
173
|
-
blanks: z4.array(BlankConfigSchema).min(1),
|
|
174
|
-
scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
|
|
175
|
-
media: MediaSchema.optional(),
|
|
176
|
-
feedback: FeedbackSchema.optional(),
|
|
177
|
-
passThreshold: z4.number().min(0).max(1).optional(),
|
|
178
|
-
locale: z4.string().optional(),
|
|
179
|
-
learningObjectives: z4.array(z4.string()).optional(),
|
|
180
|
-
difficultyLevel: z4.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, (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,791 +1247,182 @@ var DeferredScoringError = class extends Error {
|
|
|
1306
1247
|
activityType;
|
|
1307
1248
|
};
|
|
1308
1249
|
|
|
1309
|
-
// src/schemas/
|
|
1310
|
-
import { z as
|
|
1311
|
-
|
|
1312
|
-
|
|
1250
|
+
// src/schemas/multiple-choice.ts
|
|
1251
|
+
import { z as z4 } from "zod/v4";
|
|
1252
|
+
|
|
1253
|
+
// src/schemas/ai.ts
|
|
1254
|
+
import { z as z2 } from "zod/v4";
|
|
1255
|
+
var AiPermissionsSchema = z2.looseObject({
|
|
1256
|
+
explanations: z2.boolean().optional(),
|
|
1257
|
+
hints: z2.boolean().optional()
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// src/schemas/feedback.ts
|
|
1261
|
+
import { z as z3 } from "zod/v4";
|
|
1262
|
+
var FeedbackSchema = z3.looseObject({
|
|
1263
|
+
correct: z3.string().min(1).optional(),
|
|
1264
|
+
incorrect: z3.string().min(1).optional()
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// src/schemas/multiple-choice.ts
|
|
1268
|
+
var MultipleChoiceOptionMediaSchema = z4.looseObject({
|
|
1269
|
+
// No `embed`: an iframe swallows the click that selects the option, so the
|
|
1270
|
+
// learner could not choose it. No `video`: a native control bar inside the
|
|
1271
|
+
// option's label eats the same click.
|
|
1272
|
+
type: z4.enum(["image", "audio"]),
|
|
1313
1273
|
url: MediaUrlSchema,
|
|
1314
|
-
alt:
|
|
1274
|
+
alt: z4.string().min(1).optional(),
|
|
1275
|
+
captionsUrl: MediaUrlSchema.optional()
|
|
1276
|
+
}).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
|
|
1277
|
+
error: "An image option requires a non-empty `alt`.",
|
|
1278
|
+
path: ["alt"]
|
|
1279
|
+
}).refine((media) => !("playback" in media), {
|
|
1280
|
+
error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
|
|
1281
|
+
path: ["playback"]
|
|
1315
1282
|
});
|
|
1316
|
-
var
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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"]
|
|
1283
|
+
var MultipleChoiceOptionSchema = z4.looseObject({
|
|
1284
|
+
id: z4.string().min(1),
|
|
1285
|
+
text: z4.string().min(1),
|
|
1286
|
+
isCorrect: z4.boolean(),
|
|
1287
|
+
feedback: z4.string().optional(),
|
|
1288
|
+
media: MultipleChoiceOptionMediaSchema.optional()
|
|
1327
1289
|
});
|
|
1328
|
-
var
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1290
|
+
var MultipleChoiceDataSchema = z4.looseObject({
|
|
1291
|
+
schemaVersion: z4.literal("1.0"),
|
|
1292
|
+
type: z4.literal("multiple-choice"),
|
|
1293
|
+
id: z4.string().min(1),
|
|
1294
|
+
title: z4.string().min(1),
|
|
1295
|
+
question: z4.string().min(1),
|
|
1296
|
+
questionHtml: z4.string().optional(),
|
|
1297
|
+
mode: z4.enum(["single", "multi"]),
|
|
1298
|
+
options: z4.array(MultipleChoiceOptionSchema).min(2).max(26),
|
|
1299
|
+
scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
|
|
1300
|
+
media: MediaSchema.optional(),
|
|
1301
|
+
feedback: FeedbackSchema.optional(),
|
|
1302
|
+
passThreshold: z4.number().min(0).max(1).optional(),
|
|
1303
|
+
shuffle: z4.boolean().optional(),
|
|
1304
|
+
locale: z4.string().optional(),
|
|
1305
|
+
learningObjectives: z4.array(z4.string()).optional(),
|
|
1306
|
+
difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional(),
|
|
1307
|
+
ai: AiPermissionsSchema.optional()
|
|
1308
|
+
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
1309
|
+
error: "At least one option must be marked correct.",
|
|
1310
|
+
path: ["options"]
|
|
1311
|
+
}).refine(
|
|
1312
|
+
(data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
|
|
1313
|
+
{
|
|
1314
|
+
error: 'Single-select activities (mode: "single") must have exactly one correct option.',
|
|
1315
|
+
path: ["options"]
|
|
1316
|
+
}
|
|
1317
|
+
).refine((data) => new Set(data.options.map((option) => option.id)).size === data.options.length, {
|
|
1318
|
+
error: "Option ids must be unique within the activity.",
|
|
1319
|
+
path: ["options"]
|
|
1332
1320
|
});
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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;
|
|
1339
1334
|
}
|
|
1340
|
-
|
|
1335
|
+
const trimmed = text.trim();
|
|
1336
|
+
if (trimmed === "") {
|
|
1337
|
+
return 0;
|
|
1338
|
+
}
|
|
1339
|
+
return trimmed.split(/\s+/).length;
|
|
1341
1340
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
+
}
|
|
1344
1348
|
}
|
|
1345
|
-
function
|
|
1346
|
-
return
|
|
1349
|
+
function isOwnSchema(schema) {
|
|
1350
|
+
return OWN_SCHEMAS.has(schema);
|
|
1347
1351
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1352
|
+
var GENERIC_CODE = "invalid";
|
|
1353
|
+
function valueAt(root, path) {
|
|
1354
|
+
let node = root;
|
|
1355
|
+
for (const segment of path) {
|
|
1356
|
+
if (node === null || typeof node !== "object") {
|
|
1357
|
+
return void 0;
|
|
1358
|
+
}
|
|
1359
|
+
node = node[segment];
|
|
1360
|
+
}
|
|
1361
|
+
return node;
|
|
1350
1362
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
return guard;
|
|
1363
|
+
var isThenable = (value) => typeof value?.then === "function";
|
|
1364
|
+
function isZodSchema(schema) {
|
|
1365
|
+
return schema["~standard"].vendor === "zod" && typeof schema.safeParse === "function";
|
|
1355
1366
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
slowMedia: DictationSlowMediaSchema.optional(),
|
|
1365
|
-
hints: z5.looseObject({ mode: z5.literal("progressive-words") }).optional(),
|
|
1366
|
-
tolerance: DictationToleranceSchema.optional(),
|
|
1367
|
-
feedback: FeedbackSchema.optional(),
|
|
1368
|
-
passThreshold: z5.number().min(0).max(1).optional(),
|
|
1369
|
-
locale: z5.string().optional(),
|
|
1370
|
-
learningObjectives: z5.array(z5.string()).optional(),
|
|
1371
|
-
difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional(),
|
|
1372
|
-
ai: AiPermissionsSchema.optional()
|
|
1373
|
-
});
|
|
1374
|
-
var DictationDataSchema = DictationDataShape.check(
|
|
1375
|
-
checkEvenAfterIssues((ctx) => {
|
|
1376
|
-
if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
|
|
1377
|
-
return;
|
|
1367
|
+
function readSchema(schema, value, subject, options) {
|
|
1368
|
+
if (isZodSchema(schema)) {
|
|
1369
|
+
const parsed = schema.safeParse(
|
|
1370
|
+
value,
|
|
1371
|
+
options?.reportInput === true ? { reportInput: true } : void 0
|
|
1372
|
+
);
|
|
1373
|
+
if (parsed.success) {
|
|
1374
|
+
return { success: true, data: parsed.data };
|
|
1378
1375
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
if (!refused.has("acceptedTranscripts")) {
|
|
1403
|
-
const accepted = 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);
|
|
1415
|
-
if (new Set(normalized).size !== normalized.length) {
|
|
1416
|
-
report(
|
|
1417
|
-
["acceptedTranscripts"],
|
|
1418
|
-
data.acceptedTranscripts,
|
|
1419
|
-
"An accepted transcript must differ from the transcript and from the other accepted transcripts once case, punctuation and spacing are ignored."
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
const mediaParses = !refused.has("media");
|
|
1425
|
-
const slowParses = !refused.has("slowMedia");
|
|
1426
|
-
if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
|
|
1427
|
-
report(
|
|
1428
|
-
["media", "type"],
|
|
1429
|
-
data.media.type,
|
|
1430
|
-
"A dictation is dictated: its recording must be audio."
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
if (slowParses && data.slowMedia?.playback !== void 0) {
|
|
1434
|
-
report(
|
|
1435
|
-
["slowMedia", "playback"],
|
|
1436
|
-
data.slowMedia.playback,
|
|
1437
|
-
"The slow recording carries no playback policy of its own; put the policy on `media` \u2014 the slow recording follows it (except the play budget)."
|
|
1385
|
+
}
|
|
1386
|
+
const result = schema["~standard"].validate(value);
|
|
1387
|
+
if (isThenable(result)) {
|
|
1388
|
+
result.then(void 0, () => {
|
|
1389
|
+
});
|
|
1390
|
+
throw new TypeError(
|
|
1391
|
+
`${subject} has a schema whose validation returned a Promise: it validates asynchronously, or its library reported an error that way. lk-core validates synchronously: register a schema whose validation returns its result.`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
const settled = result;
|
|
1395
|
+
if (settled.issues === void 0) {
|
|
1396
|
+
return { success: true, data: settled.value };
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
success: false,
|
|
1400
|
+
issues: settled.issues.map((issue2) => {
|
|
1401
|
+
const path = (issue2.path ?? []).map(
|
|
1402
|
+
(segment) => typeof segment === "object" && segment !== null ? segment.key : segment
|
|
1438
1403
|
);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
["media"],
|
|
1458
|
-
data.media,
|
|
1459
|
-
"A slow recording accompanies a recording: add `media` first."
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
if (rulesParse && !(data.tolerance?.equivalences ?? []).every(
|
|
1464
|
-
(rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
|
|
1465
|
-
)) {
|
|
1466
|
-
report(
|
|
1467
|
-
["tolerance", "equivalences"],
|
|
1468
|
-
data.tolerance?.equivalences,
|
|
1469
|
-
"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
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
for (const field of ["media", "slowMedia"]) {
|
|
1473
|
-
const recording = data[field];
|
|
1474
|
-
for (const captions of captionFieldsOf(recording)) {
|
|
1475
|
-
report(
|
|
1476
|
-
[field, captions],
|
|
1477
|
-
recording?.[captions],
|
|
1478
|
-
"A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
if (!rulesParse || !transcriptParses) {
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
const normalizer = normalizerFor(data);
|
|
1486
|
-
const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
|
|
1487
|
-
const shown = [
|
|
1488
|
-
[["title"], refused.has("title") ? void 0 : data.title],
|
|
1489
|
-
[["media", "alt"], data.media?.alt],
|
|
1490
|
-
[["slowMedia", "alt"], data.slowMedia?.alt]
|
|
1491
|
-
];
|
|
1492
|
-
for (const [path, value] of shown) {
|
|
1493
|
-
if (value === void 0) {
|
|
1494
|
-
continue;
|
|
1495
|
-
}
|
|
1496
|
-
const { tooLong, forms } = normalizer.shown(value);
|
|
1497
|
-
if (tooLong) {
|
|
1498
|
-
report(
|
|
1499
|
-
path,
|
|
1500
|
-
value,
|
|
1501
|
-
`The title and the recording descriptions are searched for the transcript before they are shown: each is at most ${WORKING_LENGTH} characters, before and after the equivalences are applied, and the equivalences may not grow it past that on the way.`
|
|
1502
|
-
);
|
|
1503
|
-
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
1504
|
-
report(
|
|
1505
|
-
path,
|
|
1506
|
-
value,
|
|
1507
|
-
'The title and the recording descriptions are shown to the learner before they type: they must not contain the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
|
|
1508
|
-
);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
})
|
|
1512
|
-
);
|
|
1513
|
-
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1514
|
-
function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void 0) {
|
|
1515
|
-
if (!isObject(media)) {
|
|
1516
|
-
return [];
|
|
1517
|
-
}
|
|
1518
|
-
const fields = [];
|
|
1519
|
-
if (written(media.captionsUrl)) {
|
|
1520
|
-
fields.push("captionsUrl");
|
|
1521
|
-
}
|
|
1522
|
-
if (Array.isArray(media.tracks) && media.tracks.length > 0) {
|
|
1523
|
-
fields.push("tracks");
|
|
1524
|
-
}
|
|
1525
|
-
return fields;
|
|
1526
|
-
}
|
|
1527
|
-
function groupCaptionsField(group, written) {
|
|
1528
|
-
if (!isObject(group) || !isObject(group.stimulus) || !Array.isArray(group.items)) {
|
|
1529
|
-
return void 0;
|
|
1530
|
-
}
|
|
1531
|
-
const [field] = captionFieldsOf(group.stimulus.media, written);
|
|
1532
|
-
if (field === void 0) {
|
|
1533
|
-
return void 0;
|
|
1534
|
-
}
|
|
1535
|
-
const played = group.items.some(
|
|
1536
|
-
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1537
|
-
);
|
|
1538
|
-
return played ? field : void 0;
|
|
1539
|
-
}
|
|
1540
|
-
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
|
-
|
|
1542
|
-
// src/schemas/multiple-choice.ts
|
|
1543
|
-
import { z as z6 } from "zod/v4";
|
|
1544
|
-
var MultipleChoiceOptionMediaSchema = z6.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: z6.enum(["image", "audio"]),
|
|
1549
|
-
url: MediaUrlSchema,
|
|
1550
|
-
alt: z6.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 = z6.looseObject({
|
|
1560
|
-
id: z6.string().min(1),
|
|
1561
|
-
text: z6.string().min(1),
|
|
1562
|
-
isCorrect: z6.boolean(),
|
|
1563
|
-
feedback: z6.string().optional(),
|
|
1564
|
-
media: MultipleChoiceOptionMediaSchema.optional()
|
|
1565
|
-
});
|
|
1566
|
-
var MultipleChoiceDataSchema = z6.looseObject({
|
|
1567
|
-
schemaVersion: z6.literal("1.0"),
|
|
1568
|
-
type: z6.literal("multiple-choice"),
|
|
1569
|
-
id: z6.string().min(1),
|
|
1570
|
-
title: z6.string().min(1),
|
|
1571
|
-
question: z6.string().min(1),
|
|
1572
|
-
questionHtml: z6.string().optional(),
|
|
1573
|
-
mode: z6.enum(["single", "multi"]),
|
|
1574
|
-
options: z6.array(MultipleChoiceOptionSchema).min(2).max(26),
|
|
1575
|
-
scoringStrategy: z6.enum(["all-or-nothing", "partial"]),
|
|
1576
|
-
media: MediaSchema.optional(),
|
|
1577
|
-
feedback: FeedbackSchema.optional(),
|
|
1578
|
-
passThreshold: z6.number().min(0).max(1).optional(),
|
|
1579
|
-
shuffle: z6.boolean().optional(),
|
|
1580
|
-
locale: z6.string().optional(),
|
|
1581
|
-
learningObjectives: z6.array(z6.string()).optional(),
|
|
1582
|
-
difficultyLevel: z6.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
|
-
import { z as z7 } from "zod/v4";
|
|
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 = z7.strictObject({
|
|
1616
|
-
type: z7.literal("audio"),
|
|
1617
|
-
url: MediaUrlSchema,
|
|
1618
|
-
alt: z7.string().min(1).optional()
|
|
1619
|
-
});
|
|
1620
|
-
var ReferenceTextSchema = z7.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 = z7.looseObject({
|
|
1624
|
-
maxSeconds: z7.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
|
|
1625
|
-
minSeconds: z7.number().min(0).optional(),
|
|
1626
|
-
maxTakes: z7.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
|
|
1627
|
-
});
|
|
1628
|
-
var ReadAloudDimensionWeightSchema = z7.looseObject({
|
|
1629
|
-
name: z7.enum(READ_ALOUD_DIMENSIONS),
|
|
1630
|
-
weight: z7.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
|
|
1631
|
-
});
|
|
1632
|
-
var ReadAloudScoringSchema = z7.looseObject({
|
|
1633
|
-
dimensions: z7.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
|
|
1634
|
-
});
|
|
1635
|
-
function checkEvenAfterIssues2(check) {
|
|
1636
|
-
const guard = new z7.core.$ZodCheck({ check: "custom", when: () => true });
|
|
1637
|
-
guard._zod.check = check;
|
|
1638
|
-
return guard;
|
|
1639
|
-
}
|
|
1640
|
-
var ReadAloudDataShape = z7.looseObject({
|
|
1641
|
-
schemaVersion: z7.literal("1.0"),
|
|
1642
|
-
type: z7.literal("read-aloud"),
|
|
1643
|
-
id: z7.string().min(1),
|
|
1644
|
-
title: z7.string().min(1),
|
|
1645
|
-
instructions: z7.string().optional(),
|
|
1646
|
-
referenceText: ReferenceTextSchema,
|
|
1647
|
-
locale: z7.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: z7.number().min(0).max(1).optional(),
|
|
1655
|
-
feedback: FeedbackSchema.optional(),
|
|
1656
|
-
learningObjectives: z7.array(z7.string()).optional(),
|
|
1657
|
-
difficultyLevel: z7.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) => (issue2.path?.length ?? 0) === 0)) {
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
const data = ctx.value;
|
|
1666
|
-
const refused = new Set(ctx.issues.map((issue2) => String(issue2.path?.[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 (data.media?.playback?.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 data.media?.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
|
-
import { z as z8 } from "zod/v4";
|
|
1775
|
-
var GapSelectChoiceSchema = z8.looseObject({
|
|
1776
|
-
id: z8.string().min(1),
|
|
1777
|
-
text: z8.string().min(1)
|
|
1778
|
-
});
|
|
1779
|
-
var GapSelectBankSchema = z8.looseObject({
|
|
1780
|
-
id: z8.string().min(1),
|
|
1781
|
-
choices: z8.array(GapSelectChoiceSchema).min(2)
|
|
1782
|
-
});
|
|
1783
|
-
var GapSelectGapSchema = z8.looseObject({
|
|
1784
|
-
id: z8.string().min(1),
|
|
1785
|
-
choices: z8.array(GapSelectChoiceSchema).min(2).optional(),
|
|
1786
|
-
bankId: z8.string().min(1).optional(),
|
|
1787
|
-
correctChoiceId: z8.string().min(1),
|
|
1788
|
-
feedback: z8.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 banks.find((bank) => bank.id === gap.bankId)?.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, (counts.get(id) ?? 0) + 1);
|
|
1804
|
-
}
|
|
1805
|
-
return counts;
|
|
1806
|
-
}
|
|
1807
|
-
var GapSelectDataSchema = z8.looseObject({
|
|
1808
|
-
schemaVersion: z8.literal("1.0"),
|
|
1809
|
-
type: z8.literal("gap-select"),
|
|
1810
|
-
id: z8.string().min(1),
|
|
1811
|
-
title: z8.string().min(1),
|
|
1812
|
-
passage: z8.string().min(1),
|
|
1813
|
-
passageHtml: z8.string().optional(),
|
|
1814
|
-
gaps: z8.array(GapSelectGapSchema).min(1),
|
|
1815
|
-
banks: z8.array(GapSelectBankSchema).optional(),
|
|
1816
|
-
scoringStrategy: z8.enum(["all-or-nothing", "partial"]),
|
|
1817
|
-
presentation: z8.literal("dropdown").optional(),
|
|
1818
|
-
shuffleChoices: z8.boolean().optional(),
|
|
1819
|
-
media: MediaSchema.optional(),
|
|
1820
|
-
feedback: FeedbackSchema.optional(),
|
|
1821
|
-
passThreshold: z8.number().min(0).max(1).optional(),
|
|
1822
|
-
locale: z8.string().optional(),
|
|
1823
|
-
learningObjectives: z8.array(z8.string()).optional(),
|
|
1824
|
-
difficultyLevel: z8.literal([1, 2, 3, 4, 5]).optional(),
|
|
1825
|
-
ai: AiPermissionsSchema.optional()
|
|
1826
|
-
}).refine(
|
|
1827
|
-
(data) => {
|
|
1828
|
-
const ids = (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((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 = 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
|
-
import { z as z9 } from "zod/v4";
|
|
1880
|
-
var WrittenResponseRubricCriterionSchema = z9.looseObject({
|
|
1881
|
-
name: z9.string().min(1),
|
|
1882
|
-
description: z9.string().optional(),
|
|
1883
|
-
weight: z9.number().min(0)
|
|
1884
|
-
});
|
|
1885
|
-
var WrittenResponseRubricSchema = z9.looseObject({
|
|
1886
|
-
label: z9.string().optional(),
|
|
1887
|
-
criteria: z9.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
1888
|
-
});
|
|
1889
|
-
var RedactedWrittenResponseRubricCriterionSchema = z9.strictObject({
|
|
1890
|
-
name: z9.string().min(1),
|
|
1891
|
-
description: z9.string().optional(),
|
|
1892
|
-
weight: z9.number().min(0)
|
|
1893
|
-
});
|
|
1894
|
-
var RedactedWrittenResponseRubricSchema = z9.strictObject({
|
|
1895
|
-
label: z9.string().optional(),
|
|
1896
|
-
criteria: z9.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
|
|
1897
|
-
});
|
|
1898
|
-
var WrittenResponseDataSchema = z9.looseObject({
|
|
1899
|
-
schemaVersion: z9.literal("1.0"),
|
|
1900
|
-
type: z9.literal("written-response"),
|
|
1901
|
-
id: z9.string().min(1),
|
|
1902
|
-
title: z9.string().min(1),
|
|
1903
|
-
prompt: z9.string(),
|
|
1904
|
-
promptHtml: z9.string().optional(),
|
|
1905
|
-
minWords: z9.number().int().min(0),
|
|
1906
|
-
maxWords: z9.number().int().min(1),
|
|
1907
|
-
rubric: WrittenResponseRubricSchema.optional(),
|
|
1908
|
-
languageTarget: z9.string().optional(),
|
|
1909
|
-
media: MediaSchema.optional(),
|
|
1910
|
-
feedback: FeedbackSchema.optional(),
|
|
1911
|
-
passThreshold: z9.number().min(0).max(1).optional(),
|
|
1912
|
-
locale: z9.string().optional(),
|
|
1913
|
-
learningObjectives: z9.array(z9.string()).optional(),
|
|
1914
|
-
difficultyLevel: z9.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
|
-
import { z as z10 } from "zod/v4";
|
|
1923
|
-
var redactedBase = {
|
|
1924
|
-
/** Marker distinguishing a redacted projection from full activity data. */
|
|
1925
|
-
redacted: z10.literal(true),
|
|
1926
|
-
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
1927
|
-
slotKey: z10.string().min(1).optional(),
|
|
1928
|
-
schemaVersion: z10.literal("1.0"),
|
|
1929
|
-
id: z10.string().min(1),
|
|
1930
|
-
title: z10.string().min(1),
|
|
1931
|
-
media: RedactedMediaSchema.optional(),
|
|
1932
|
-
passThreshold: z10.number().min(0).max(1).optional(),
|
|
1933
|
-
locale: z10.string().optional(),
|
|
1934
|
-
learningObjectives: z10.array(z10.string()).optional(),
|
|
1935
|
-
difficultyLevel: z10.literal([1, 2, 3, 4, 5]).optional(),
|
|
1936
|
-
ai: z10.strictObject({ explanations: z10.boolean().optional(), hints: z10.boolean().optional() }).optional()
|
|
1937
|
-
};
|
|
1938
|
-
var RedactedMultipleChoiceOptionMediaSchema = z10.strictObject({
|
|
1939
|
-
type: z10.enum(["image", "audio"]),
|
|
1940
|
-
url: z10.string().min(1),
|
|
1941
|
-
alt: z10.string().min(1).optional(),
|
|
1942
|
-
captionsUrl: z10.string().min(1).optional()
|
|
1943
|
-
});
|
|
1944
|
-
var RedactedMultipleChoiceOptionSchema = z10.strictObject({
|
|
1945
|
-
id: z10.string().min(1),
|
|
1946
|
-
text: z10.string().min(1),
|
|
1947
|
-
media: RedactedMultipleChoiceOptionMediaSchema.optional()
|
|
1948
|
-
});
|
|
1949
|
-
var RedactedMultipleChoiceDataSchema = z10.strictObject({
|
|
1950
|
-
...redactedBase,
|
|
1951
|
-
type: z10.literal("multiple-choice"),
|
|
1952
|
-
question: z10.string().min(1),
|
|
1953
|
-
questionHtml: z10.string().optional(),
|
|
1954
|
-
mode: z10.enum(["single", "multi"]),
|
|
1955
|
-
options: z10.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
|
|
1956
|
-
shuffle: z10.boolean().optional()
|
|
1957
|
-
});
|
|
1958
|
-
var RedactedBlankConfigSchema = z10.strictObject({
|
|
1959
|
-
id: z10.string().min(1),
|
|
1960
|
-
hint: z10.string().optional()
|
|
1961
|
-
});
|
|
1962
|
-
var RedactedFillInTheBlanksDataSchema = z10.strictObject({
|
|
1963
|
-
...redactedBase,
|
|
1964
|
-
type: z10.literal("fill-in-the-blanks"),
|
|
1965
|
-
passage: z10.string().min(1),
|
|
1966
|
-
passageHtml: z10.string().optional(),
|
|
1967
|
-
blanks: z10.array(RedactedBlankConfigSchema).min(1)
|
|
1968
|
-
});
|
|
1969
|
-
var RedactedGapSelectChoiceSchema = z10.strictObject({
|
|
1970
|
-
id: z10.string().min(1),
|
|
1971
|
-
text: z10.string().min(1)
|
|
1972
|
-
});
|
|
1973
|
-
var RedactedGapSelectBankSchema = z10.strictObject({
|
|
1974
|
-
id: z10.string().min(1),
|
|
1975
|
-
choices: z10.array(RedactedGapSelectChoiceSchema).min(2)
|
|
1976
|
-
});
|
|
1977
|
-
var RedactedGapSelectGapSchema = z10.strictObject({
|
|
1978
|
-
id: z10.string().min(1),
|
|
1979
|
-
choices: z10.array(RedactedGapSelectChoiceSchema).min(2).optional(),
|
|
1980
|
-
bankId: z10.string().min(1).optional()
|
|
1981
|
-
});
|
|
1982
|
-
var RedactedGapSelectDataSchema = z10.strictObject({
|
|
1983
|
-
...redactedBase,
|
|
1984
|
-
type: z10.literal("gap-select"),
|
|
1985
|
-
passage: z10.string().min(1),
|
|
1986
|
-
passageHtml: z10.string().optional(),
|
|
1987
|
-
gaps: z10.array(RedactedGapSelectGapSchema).min(1),
|
|
1988
|
-
banks: z10.array(RedactedGapSelectBankSchema).optional(),
|
|
1989
|
-
presentation: z10.literal("dropdown").optional(),
|
|
1990
|
-
shuffleChoices: z10.boolean().optional()
|
|
1991
|
-
});
|
|
1992
|
-
var RedactedWrittenResponseDataSchema = z10.strictObject({
|
|
1993
|
-
...redactedBase,
|
|
1994
|
-
type: z10.literal("written-response"),
|
|
1995
|
-
prompt: z10.string(),
|
|
1996
|
-
promptHtml: z10.string().optional(),
|
|
1997
|
-
minWords: z10.number().int().min(0),
|
|
1998
|
-
maxWords: z10.number().int().min(1),
|
|
1999
|
-
rubric: RedactedWrittenResponseRubricSchema.optional(),
|
|
2000
|
-
languageTarget: z10.string().optional()
|
|
2001
|
-
});
|
|
2002
|
-
var RedactedDictationSlowMediaSchema = z10.strictObject({
|
|
2003
|
-
type: z10.literal("audio"),
|
|
2004
|
-
url: MediaUrlSchema,
|
|
2005
|
-
alt: z10.string().min(1).optional()
|
|
2006
|
-
});
|
|
2007
|
-
var RedactedDictationDataSchema = z10.strictObject({
|
|
2008
|
-
...redactedBase,
|
|
2009
|
-
type: z10.literal("dictation"),
|
|
2010
|
-
slowMedia: RedactedDictationSlowMediaSchema.optional(),
|
|
2011
|
-
hints: z10.strictObject({ mode: z10.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: data.media?.[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 && data.media?.playback?.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 = z10.strictObject({
|
|
2040
|
-
...redactedBase,
|
|
2041
|
-
type: z10.literal("read-aloud"),
|
|
2042
|
-
locale: z10.string().regex(CANONICAL_LOCALE_RE),
|
|
2043
|
-
instructions: z10.string().optional(),
|
|
2044
|
-
referenceText: z10.string().min(1),
|
|
2045
|
-
slowMedia: ReadAloudSlowMediaSchema.optional(),
|
|
2046
|
-
recording: z10.strictObject({
|
|
2047
|
-
maxSeconds: z10.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
|
|
2048
|
-
minSeconds: z10.number().min(0).optional(),
|
|
2049
|
-
maxTakes: z10.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
|
|
2050
|
-
}),
|
|
2051
|
-
scoring: z10.strictObject({
|
|
2052
|
-
dimensions: z10.array(
|
|
2053
|
-
z10.strictObject({
|
|
2054
|
-
name: z10.enum(READ_ALOUD_DIMENSIONS),
|
|
2055
|
-
weight: z10.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 && data.media?.playback?.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 === data.media?.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
|
-
});
|
|
1404
|
+
const code = issue2.code;
|
|
1405
|
+
return {
|
|
1406
|
+
path,
|
|
1407
|
+
message: issue2.message,
|
|
1408
|
+
code: typeof code === "string" && code !== "" ? code : GENERIC_CODE,
|
|
1409
|
+
input: valueAt(value, path)
|
|
1410
|
+
};
|
|
1411
|
+
})
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function isStandardSchema(value) {
|
|
1415
|
+
const props = value?.["~standard"];
|
|
1416
|
+
return props?.version === 1 && typeof props.validate === "function";
|
|
1417
|
+
}
|
|
1418
|
+
function standardJsonSchemaOf(schema) {
|
|
1419
|
+
const converter = schema["~standard"].jsonSchema;
|
|
1420
|
+
if (typeof converter?.output !== "function") {
|
|
1421
|
+
return void 0;
|
|
2092
1422
|
}
|
|
2093
|
-
});
|
|
1423
|
+
const generated = converter.output({ target: "draft-07" });
|
|
1424
|
+
return generated !== null && typeof generated === "object" && !Array.isArray(generated) ? generated : void 0;
|
|
1425
|
+
}
|
|
2094
1426
|
|
|
2095
1427
|
// src/scoring/text-match.ts
|
|
2096
1428
|
var COMBINING_MARKS_RE = /\p{M}/gu;
|
|
@@ -2143,66 +1475,326 @@ function levenshteinDistance(a, b, max) {
|
|
|
2143
1475
|
current.push(value);
|
|
2144
1476
|
if (value < rowMin) rowMin = value;
|
|
2145
1477
|
}
|
|
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" };
|
|
1478
|
+
if (rowMin > max) return max + 1;
|
|
1479
|
+
previous = current;
|
|
1480
|
+
}
|
|
1481
|
+
return previous[b.length];
|
|
1482
|
+
}
|
|
1483
|
+
function matchText(input, accepted, policy = {}) {
|
|
1484
|
+
const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
|
|
1485
|
+
const baselineInput = applyBaseline(input, policy);
|
|
1486
|
+
const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
|
|
1487
|
+
if (baselineAccepted.some((answer) => answer === baselineInput)) {
|
|
1488
|
+
return { matched: true, via: "exact" };
|
|
1489
|
+
}
|
|
1490
|
+
const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
|
|
1491
|
+
const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
|
|
1492
|
+
const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
|
|
1493
|
+
if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
|
|
1494
|
+
return { matched: true, via: "normalized" };
|
|
1495
|
+
}
|
|
1496
|
+
const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
|
|
1497
|
+
const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
|
|
1498
|
+
if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
|
|
1499
|
+
return { matched: true, via: "folded" };
|
|
1500
|
+
}
|
|
1501
|
+
const maxDistance = policy.levenshtein ?? 0;
|
|
1502
|
+
if (maxDistance > 0 && foldedInput.trim().length > 0 && foldedAccepted.some(
|
|
1503
|
+
(answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
|
|
1504
|
+
)) {
|
|
1505
|
+
return { matched: true, via: "fuzzy" };
|
|
1506
|
+
}
|
|
1507
|
+
return { matched: false, via: "none" };
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/registry/registry.ts
|
|
1511
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1512
|
+
function defineActivityType(descriptor) {
|
|
1513
|
+
return descriptor;
|
|
1514
|
+
}
|
|
1515
|
+
function registerActivityType(descriptor) {
|
|
1516
|
+
if (descriptor.type === "item-group") {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`"item-group" is reserved for the SDK's item-group container (see ItemGroup) and cannot be registered as an activity type.`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
if (!isStandardSchema(descriptor.schema)) {
|
|
1522
|
+
throw new TypeError(
|
|
1523
|
+
`Activity type "${descriptor.type}": \`schema\` must implement Standard Schema v1 (https://standardschema.dev), as a zod 4, valibot or ArkType schema does.`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
if (descriptor.redactedSchema !== void 0 && !isStandardSchema(descriptor.redactedSchema)) {
|
|
1527
|
+
throw new TypeError(
|
|
1528
|
+
`Activity type "${descriptor.type}": \`redactedSchema\` must implement Standard Schema v1 (https://standardschema.dev), as a zod 4, valibot or ArkType schema does.`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
if (descriptor.jsonSchema !== void 0 && (descriptor.jsonSchema === null || typeof descriptor.jsonSchema !== "object" || Array.isArray(descriptor.jsonSchema))) {
|
|
1532
|
+
throw new TypeError(
|
|
1533
|
+
`Activity type "${descriptor.type}": \`jsonSchema\` must be a JSON Schema object.`
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
const existing = registry.get(descriptor.type);
|
|
1537
|
+
if (existing !== void 0) {
|
|
1538
|
+
if (existing === descriptor) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
`Activity type "${descriptor.type}" is already registered. Registering a different descriptor for an existing type is not allowed.`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
registry.set(descriptor.type, descriptor);
|
|
1546
|
+
}
|
|
1547
|
+
function getActivityTypeDescriptor(type) {
|
|
1548
|
+
return registry.get(type);
|
|
1549
|
+
}
|
|
1550
|
+
function registeredActivityTypes() {
|
|
1551
|
+
return [...registry.keys()];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/schemas/dictation.ts
|
|
1555
|
+
import { z as z6 } from "zod/v4";
|
|
1556
|
+
|
|
1557
|
+
// src/schemas/after-issues.ts
|
|
1558
|
+
import { z as z5 } from "zod/v4";
|
|
1559
|
+
function checkEvenAfterIssues(check) {
|
|
1560
|
+
const guard = new z5.core.$ZodCheck({ check: "custom", when: () => true });
|
|
1561
|
+
guard._zod.check = check;
|
|
1562
|
+
return guard;
|
|
1563
|
+
}
|
|
1564
|
+
function parsedFields(payload) {
|
|
1565
|
+
const refused = new Set(payload.issues.map((issue2) => String(issue2.path?.[0])));
|
|
1566
|
+
const data = { ...payload.value };
|
|
1567
|
+
for (const key of refused) {
|
|
1568
|
+
delete data[key];
|
|
1569
|
+
}
|
|
1570
|
+
return { data, refused };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/schemas/dictation.ts
|
|
1574
|
+
var DictationSlowMediaSchema = z6.looseObject({
|
|
1575
|
+
type: z6.literal("audio"),
|
|
1576
|
+
url: MediaUrlSchema,
|
|
1577
|
+
alt: z6.string().min(1).optional()
|
|
1578
|
+
});
|
|
1579
|
+
var RuleTextSchema = z6.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
|
|
1580
|
+
var TranscriptTextSchema = z6.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
|
|
1581
|
+
var DictationEquivalenceSchema = z6.looseObject({
|
|
1582
|
+
from: RuleTextSchema,
|
|
1583
|
+
to: RuleTextSchema
|
|
1584
|
+
}).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
|
|
1585
|
+
error: `A rule rewrites at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
|
|
1586
|
+
path: ["from"]
|
|
1587
|
+
}).refine((rule) => codePointLength(rule.to) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
|
|
1588
|
+
error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
|
|
1589
|
+
path: ["to"]
|
|
1590
|
+
});
|
|
1591
|
+
var DictationToleranceSchema = z6.looseObject({
|
|
1592
|
+
equivalences: z6.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
|
|
1593
|
+
error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
|
|
1594
|
+
}).optional()
|
|
1595
|
+
});
|
|
1596
|
+
var normalizers = /* @__PURE__ */ new WeakMap();
|
|
1597
|
+
function normalizerFor(data) {
|
|
1598
|
+
let normalizer = normalizers.get(data);
|
|
1599
|
+
if (normalizer === void 0) {
|
|
1600
|
+
normalizer = dictationNormalizer(data.tolerance);
|
|
1601
|
+
normalizers.set(data, normalizer);
|
|
1602
|
+
}
|
|
1603
|
+
return normalizer;
|
|
1604
|
+
}
|
|
1605
|
+
function measure(data, text) {
|
|
1606
|
+
return normalizerFor(data).measure(text);
|
|
1607
|
+
}
|
|
1608
|
+
function candidatesOf(data) {
|
|
1609
|
+
return [data.transcript, ...data.acceptedTranscripts ?? []];
|
|
1610
|
+
}
|
|
1611
|
+
function isEmpty(measured) {
|
|
1612
|
+
return measured.normalized === "";
|
|
1613
|
+
}
|
|
1614
|
+
var DictationDataShape = z6.looseObject({
|
|
1615
|
+
schemaVersion: z6.literal("1.0"),
|
|
1616
|
+
type: z6.literal("dictation"),
|
|
1617
|
+
id: z6.string().min(1),
|
|
1618
|
+
title: z6.string().min(1),
|
|
1619
|
+
transcript: TranscriptTextSchema,
|
|
1620
|
+
acceptedTranscripts: z6.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
|
|
1621
|
+
media: MediaSchema.optional(),
|
|
1622
|
+
slowMedia: DictationSlowMediaSchema.optional(),
|
|
1623
|
+
hints: z6.looseObject({ mode: z6.literal("progressive-words") }).optional(),
|
|
1624
|
+
tolerance: DictationToleranceSchema.optional(),
|
|
1625
|
+
feedback: FeedbackSchema.optional(),
|
|
1626
|
+
passThreshold: z6.number().min(0).max(1).optional(),
|
|
1627
|
+
locale: z6.string().optional(),
|
|
1628
|
+
learningObjectives: z6.array(z6.string()).optional(),
|
|
1629
|
+
difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional(),
|
|
1630
|
+
ai: AiPermissionsSchema.optional()
|
|
1631
|
+
});
|
|
1632
|
+
var DictationDataSchema = DictationDataShape.check(
|
|
1633
|
+
checkEvenAfterIssues((ctx) => {
|
|
1634
|
+
if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const { data, refused } = parsedFields(ctx);
|
|
1638
|
+
const report = (path, input, message) => {
|
|
1639
|
+
ctx.issues.push({ code: "custom", input, message, path: [...path] });
|
|
1640
|
+
};
|
|
1641
|
+
const rulesParse = !refused.has("tolerance");
|
|
1642
|
+
const transcriptParses = !refused.has("transcript");
|
|
1643
|
+
if (rulesParse && transcriptParses) {
|
|
1644
|
+
const transcript = measure(data, data.transcript);
|
|
1645
|
+
if (isEmpty(transcript)) {
|
|
1646
|
+
report(
|
|
1647
|
+
["transcript"],
|
|
1648
|
+
data.transcript,
|
|
1649
|
+
"The transcript must contain something to type: after ignoring case, punctuation and spacing nothing is left."
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
if (transcript.tooLong) {
|
|
1653
|
+
report(
|
|
1654
|
+
["transcript"],
|
|
1655
|
+
data.transcript,
|
|
1656
|
+
`A transcript is at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
if (!refused.has("acceptedTranscripts")) {
|
|
1660
|
+
const accepted = data.acceptedTranscripts ?? [];
|
|
1661
|
+
if (accepted.some((entry) => {
|
|
1662
|
+
const measured = measure(data, entry);
|
|
1663
|
+
return measured.tooLong || isEmpty(measured);
|
|
1664
|
+
})) {
|
|
1665
|
+
report(
|
|
1666
|
+
["acceptedTranscripts"],
|
|
1667
|
+
data.acceptedTranscripts,
|
|
1668
|
+
`An accepted transcript must contain something to type and be at most ${DICTATION_MAX_TRANSCRIPT_LENGTH} characters, before and after its equivalences are applied, and its equivalences may not grow it past ${WORKING_LENGTH} characters on the way.`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const normalized = candidatesOf(data).map((entry) => measure(data, entry).normalized).filter((entry) => entry !== null);
|
|
1672
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
1673
|
+
report(
|
|
1674
|
+
["acceptedTranscripts"],
|
|
1675
|
+
data.acceptedTranscripts,
|
|
1676
|
+
"An accepted transcript must differ from the transcript and from the other accepted transcripts once case, punctuation and spacing are ignored."
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const mediaParses = !refused.has("media");
|
|
1682
|
+
const slowParses = !refused.has("slowMedia");
|
|
1683
|
+
if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
|
|
1684
|
+
report(
|
|
1685
|
+
["media", "type"],
|
|
1686
|
+
data.media.type,
|
|
1687
|
+
"A dictation is dictated: its recording must be audio."
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
if (slowParses && data.slowMedia?.playback !== void 0) {
|
|
1691
|
+
report(
|
|
1692
|
+
["slowMedia", "playback"],
|
|
1693
|
+
data.slowMedia.playback,
|
|
1694
|
+
"The slow recording carries no playback policy of its own; put the policy on `media` \u2014 the slow recording follows it (except the play budget)."
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
if (mediaParses && slowParses && data.slowMedia !== void 0) {
|
|
1698
|
+
if (data.media?.playback?.maxPlays !== void 0) {
|
|
1699
|
+
report(
|
|
1700
|
+
["slowMedia"],
|
|
1701
|
+
data.slowMedia,
|
|
1702
|
+
"A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays` (a budgeted recording can still offer the transport's 0.75\xD7 speed when `rate` is `allow`)."
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
|
|
1706
|
+
report(
|
|
1707
|
+
["slowMedia", "url"],
|
|
1708
|
+
data.slowMedia.url,
|
|
1709
|
+
"The slow recording must be a different file from the recording."
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
if (data.media === void 0) {
|
|
1713
|
+
report(
|
|
1714
|
+
["media"],
|
|
1715
|
+
data.media,
|
|
1716
|
+
"A slow recording accompanies a recording: add `media` first."
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (rulesParse && !(data.tolerance?.equivalences ?? []).every(
|
|
1721
|
+
(rule) => preStripNormalize(rule.from) !== "" && normalizeDictationText(rule.to) !== ""
|
|
1722
|
+
)) {
|
|
1723
|
+
report(
|
|
1724
|
+
["tolerance", "equivalences"],
|
|
1725
|
+
data.tolerance?.equivalences,
|
|
1726
|
+
"An equivalence must name a word, phrase or symbol to rewrite, and rewrite it to something that survives normalisation (not only punctuation or spaces)."
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
for (const field of ["media", "slowMedia"]) {
|
|
1730
|
+
const recording = data[field];
|
|
1731
|
+
for (const captions of captionFieldsOf(recording)) {
|
|
1732
|
+
report(
|
|
1733
|
+
[field, captions],
|
|
1734
|
+
recording?.[captions],
|
|
1735
|
+
"A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (!rulesParse || !transcriptParses) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const normalizer = normalizerFor(data);
|
|
1743
|
+
const candidates = candidatesOf(data).map((entry) => normalizer.measure(entry));
|
|
1744
|
+
const shown = [
|
|
1745
|
+
[["title"], refused.has("title") ? void 0 : data.title],
|
|
1746
|
+
[["media", "alt"], data.media?.alt],
|
|
1747
|
+
[["slowMedia", "alt"], data.slowMedia?.alt]
|
|
1748
|
+
];
|
|
1749
|
+
for (const [path, value] of shown) {
|
|
1750
|
+
if (value === void 0) {
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
const { tooLong, forms } = normalizer.shown(value);
|
|
1754
|
+
if (tooLong) {
|
|
1755
|
+
report(
|
|
1756
|
+
path,
|
|
1757
|
+
value,
|
|
1758
|
+
`The title and the recording descriptions are searched for the transcript before they are shown: each is at most ${WORKING_LENGTH} characters, before and after the equivalences are applied, and the equivalences may not grow it past that on the way.`
|
|
1759
|
+
);
|
|
1760
|
+
} else if (candidates.some((candidate) => revealsCandidate(forms, candidate))) {
|
|
1761
|
+
report(
|
|
1762
|
+
path,
|
|
1763
|
+
value,
|
|
1764
|
+
'The title and the recording descriptions are shown to the learner before they type: they must not contain the transcript. Describe the recording ("Recording, normal speed"), never transcribe it.'
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1771
|
+
function captionFieldsOf(media, written = (captionsUrl) => captionsUrl !== void 0) {
|
|
1772
|
+
if (!isObject(media)) {
|
|
1773
|
+
return [];
|
|
2163
1774
|
}
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return { matched: true, via: "folded" };
|
|
1775
|
+
const fields = [];
|
|
1776
|
+
if (written(media.captionsUrl)) {
|
|
1777
|
+
fields.push("captionsUrl");
|
|
2168
1778
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
(answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
|
|
2172
|
-
)) {
|
|
2173
|
-
return { matched: true, via: "fuzzy" };
|
|
1779
|
+
if (Array.isArray(media.tracks) && media.tracks.length > 0) {
|
|
1780
|
+
fields.push("tracks");
|
|
2174
1781
|
}
|
|
2175
|
-
return
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
// src/registry/registry.ts
|
|
2179
|
-
var registry = /* @__PURE__ */ new Map();
|
|
2180
|
-
function defineActivityType(descriptor) {
|
|
2181
|
-
return descriptor;
|
|
1782
|
+
return fields;
|
|
2182
1783
|
}
|
|
2183
|
-
function
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
2186
|
-
`"item-group" is reserved for the SDK's item-group container (see ItemGroup) and cannot be registered as an activity type.`
|
|
2187
|
-
);
|
|
1784
|
+
function groupCaptionsField(group, written) {
|
|
1785
|
+
if (!isObject(group) || !isObject(group.stimulus) || !Array.isArray(group.items)) {
|
|
1786
|
+
return void 0;
|
|
2188
1787
|
}
|
|
2189
|
-
const
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
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
|
-
);
|
|
1788
|
+
const [field] = captionFieldsOf(group.stimulus.media, written);
|
|
1789
|
+
if (field === void 0) {
|
|
1790
|
+
return void 0;
|
|
2197
1791
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
return
|
|
2202
|
-
}
|
|
2203
|
-
function registeredActivityTypes() {
|
|
2204
|
-
return [...registry.keys()];
|
|
1792
|
+
const played = group.items.some(
|
|
1793
|
+
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1794
|
+
);
|
|
1795
|
+
return played ? field : void 0;
|
|
2205
1796
|
}
|
|
1797
|
+
var GROUP_CAPTIONS_REVEAL_DICTATION = "The stimulus recording carries captions, and a dictation in this group plays that recording: the captions are the answer. Remove the captions, or give the dictation its own recording.";
|
|
2206
1798
|
|
|
2207
1799
|
// src/authoring/issues.ts
|
|
2208
1800
|
var DRAFT_ISSUE_SEVERITY = {
|
|
@@ -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
|
|
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 =
|
|
1972
|
+
const value = valueAt2(root, path);
|
|
2381
1973
|
const index = path.at(-1);
|
|
2382
|
-
const list =
|
|
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
|
+
import { z as z7 } from "zod/v4";
|
|
2503
|
+
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
2504
|
+
var TextMatchPolicySchema = z7.looseObject({
|
|
2505
|
+
caseSensitive: z7.boolean().optional(),
|
|
2506
|
+
trim: z7.boolean().optional(),
|
|
2507
|
+
normalize: z7.enum(["none", "NFC", "NFKC"]).optional(),
|
|
2508
|
+
foldDiacritics: z7.boolean().optional(),
|
|
2509
|
+
collapseInnerWhitespace: z7.boolean().optional(),
|
|
2510
|
+
ignorePunctuation: z7.boolean().optional(),
|
|
2511
|
+
levenshtein: z7.number().int().min(0).optional(),
|
|
2512
|
+
locale: z7.string().optional()
|
|
2513
|
+
});
|
|
2514
|
+
var BlankConfigSchema = z7.looseObject({
|
|
2515
|
+
id: z7.string().min(1),
|
|
2516
|
+
acceptedAnswers: z7.array(
|
|
2517
|
+
z7.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
2518
|
+
error: "Accepted answers must contain non-whitespace characters."
|
|
2519
|
+
})
|
|
2520
|
+
).min(1),
|
|
2521
|
+
caseSensitive: z7.boolean().optional(),
|
|
2522
|
+
trimWhitespace: z7.boolean().optional(),
|
|
2523
|
+
match: TextMatchPolicySchema.optional(),
|
|
2524
|
+
hint: z7.string().optional(),
|
|
2525
|
+
feedback: z7.string().optional()
|
|
2526
|
+
});
|
|
2527
|
+
var FillInTheBlanksDataSchema = z7.looseObject({
|
|
2528
|
+
schemaVersion: z7.literal("1.0"),
|
|
2529
|
+
type: z7.literal("fill-in-the-blanks"),
|
|
2530
|
+
id: z7.string().min(1),
|
|
2531
|
+
title: z7.string().min(1),
|
|
2532
|
+
passage: z7.string().min(1),
|
|
2533
|
+
passageHtml: z7.string().optional(),
|
|
2534
|
+
blanks: z7.array(BlankConfigSchema).min(1),
|
|
2535
|
+
scoringStrategy: z7.enum(["all-or-nothing", "partial"]),
|
|
2536
|
+
media: MediaSchema.optional(),
|
|
2537
|
+
feedback: FeedbackSchema.optional(),
|
|
2538
|
+
passThreshold: z7.number().min(0).max(1).optional(),
|
|
2539
|
+
locale: z7.string().optional(),
|
|
2540
|
+
learningObjectives: z7.array(z7.string()).optional(),
|
|
2541
|
+
difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional(),
|
|
2542
|
+
ai: AiPermissionsSchema.optional()
|
|
2543
|
+
}).refine(
|
|
2544
|
+
(data) => {
|
|
2545
|
+
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
2546
|
+
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
2547
|
+
const id = match[1];
|
|
2548
|
+
placeholderCounts2.set(id, (placeholderCounts2.get(id) ?? 0) + 1);
|
|
2549
|
+
}
|
|
2550
|
+
const blankIds = data.blanks.map((blank) => blank.id);
|
|
2551
|
+
if (new Set(blankIds).size !== blankIds.length) {
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2554
|
+
if (placeholderCounts2.size !== blankIds.length) {
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
return blankIds.every((id) => placeholderCounts2.get(id) === 1);
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
2561
|
+
path: ["passage"]
|
|
2562
|
+
}
|
|
2563
|
+
);
|
|
2564
|
+
|
|
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 =
|
|
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
|
|
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
|
+
import { z as z8 } from "zod/v4";
|
|
3179
|
+
|
|
3180
|
+
// src/scoring/speech/locale.ts
|
|
3181
|
+
var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
|
|
3182
|
+
|
|
3183
|
+
// src/schemas/read-aloud.ts
|
|
3184
|
+
var READ_ALOUD_DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
|
|
3185
|
+
var READ_ALOUD_MAX_DIMENSIONS = READ_ALOUD_DIMENSIONS.length;
|
|
3186
|
+
var ReadAloudSlowMediaSchema = z8.strictObject({
|
|
3187
|
+
type: z8.literal("audio"),
|
|
3188
|
+
url: MediaUrlSchema,
|
|
3189
|
+
alt: z8.string().min(1).optional()
|
|
3190
|
+
});
|
|
3191
|
+
var ReferenceTextSchema = z8.string().min(1).refine((text) => text.trim() !== "", {
|
|
3192
|
+
error: "The text to read aloud must contain something to read."
|
|
3193
|
+
}).meta({ maxLength: READ_ALOUD_MAX_REFERENCE_LENGTH });
|
|
3194
|
+
var RecordingBoundsSchema = z8.looseObject({
|
|
3195
|
+
maxSeconds: z8.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
|
|
3196
|
+
minSeconds: z8.number().min(0).optional(),
|
|
3197
|
+
maxTakes: z8.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
|
|
3198
|
+
});
|
|
3199
|
+
var ReadAloudDimensionWeightSchema = z8.looseObject({
|
|
3200
|
+
name: z8.enum(READ_ALOUD_DIMENSIONS),
|
|
3201
|
+
weight: z8.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
|
|
3202
|
+
});
|
|
3203
|
+
var ReadAloudScoringSchema = z8.looseObject({
|
|
3204
|
+
dimensions: z8.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
|
|
3205
|
+
});
|
|
3206
|
+
var ReadAloudDataShape = z8.looseObject({
|
|
3207
|
+
schemaVersion: z8.literal("1.0"),
|
|
3208
|
+
type: z8.literal("read-aloud"),
|
|
3209
|
+
id: z8.string().min(1),
|
|
3210
|
+
title: z8.string().min(1),
|
|
3211
|
+
instructions: z8.string().optional(),
|
|
3212
|
+
referenceText: ReferenceTextSchema,
|
|
3213
|
+
locale: z8.string().regex(CANONICAL_LOCALE_RE, {
|
|
3214
|
+
error: `The locale must be a BCP 47 tag in canonical form, with a region: "en-US", "es-419", "zh-Hant-TW". Pronunciation is assessed against one locale's speech, and "en" does not say whose.`
|
|
3215
|
+
}),
|
|
3216
|
+
media: MediaSchema.optional(),
|
|
3217
|
+
slowMedia: ReadAloudSlowMediaSchema.optional(),
|
|
3218
|
+
recording: RecordingBoundsSchema,
|
|
3219
|
+
scoring: ReadAloudScoringSchema,
|
|
3220
|
+
passThreshold: z8.number().min(0).max(1).optional(),
|
|
3221
|
+
feedback: FeedbackSchema.optional(),
|
|
3222
|
+
learningObjectives: z8.array(z8.string()).optional(),
|
|
3223
|
+
difficultyLevel: z8.literal([1, 2, 3, 4, 5]).optional(),
|
|
3224
|
+
ai: AiPermissionsSchema.optional()
|
|
3225
|
+
});
|
|
3226
|
+
var ReadAloudDataSchema = ReadAloudDataShape.check(
|
|
3227
|
+
checkEvenAfterIssues((ctx) => {
|
|
3228
|
+
if (ctx.issues.some((issue2) => (issue2.path?.length ?? 0) === 0)) {
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
const { data, refused } = parsedFields(ctx);
|
|
3232
|
+
const report = (path, input, message) => {
|
|
3233
|
+
ctx.issues.push({ code: "custom", input, message, path: [...path] });
|
|
3234
|
+
};
|
|
3235
|
+
if (!refused.has("referenceText")) {
|
|
3236
|
+
const text = data.referenceText;
|
|
3237
|
+
const measured = dictationNormalizer(void 0).measure(text);
|
|
3238
|
+
if (measured.tooLong) {
|
|
3239
|
+
report(
|
|
3240
|
+
["referenceText"],
|
|
3241
|
+
text,
|
|
3242
|
+
`The text to read aloud is at most ${READ_ALOUD_MAX_REFERENCE_LENGTH} characters, before and after normalisation.`
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
if (measured.normalized === "") {
|
|
3246
|
+
report(
|
|
3247
|
+
["referenceText"],
|
|
3248
|
+
text,
|
|
3249
|
+
"The text to read aloud must contain a word: after ignoring punctuation and spacing nothing is left, so no word would be marked and every reading would align as one insertion after another."
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
3252
|
+
if (containsUnspacedScript(text)) {
|
|
3253
|
+
report(
|
|
3254
|
+
["referenceText"],
|
|
3255
|
+
text,
|
|
3256
|
+
"A read-aloud item needs a script written with spaces between its words. The text is compared word by word, and a run of Han, kana, Thai, Lao, Khmer or Myanmar letters is one word however many it holds."
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
const mediaParses = !refused.has("media");
|
|
3261
|
+
const slowParses = !refused.has("slowMedia");
|
|
3262
|
+
if (mediaParses && data.media !== void 0 && data.media.type !== "audio") {
|
|
3263
|
+
report(
|
|
3264
|
+
["media", "type"],
|
|
3265
|
+
data.media.type,
|
|
3266
|
+
"A read-aloud model is read aloud: its recording must be audio."
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
if (mediaParses && slowParses && data.slowMedia !== void 0) {
|
|
3270
|
+
if (data.media === void 0) {
|
|
3271
|
+
report(
|
|
3272
|
+
["media"],
|
|
3273
|
+
data.media,
|
|
3274
|
+
"A slow model recording accompanies a recording: add `media` first."
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
if (data.media?.playback?.maxPlays !== void 0) {
|
|
3278
|
+
report(
|
|
3279
|
+
["slowMedia"],
|
|
3280
|
+
data.slowMedia,
|
|
3281
|
+
"A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content. Drop `slowMedia`, or drop `maxPlays`."
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
if (typeof data.slowMedia.url === "string" && typeof data.media?.url === "string" && data.slowMedia.url === data.media.url) {
|
|
3285
|
+
report(
|
|
3286
|
+
["slowMedia", "url"],
|
|
3287
|
+
data.slowMedia.url,
|
|
3288
|
+
"The slow model recording must be a different file from the recording."
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
if (!refused.has("scoring")) {
|
|
3293
|
+
const dimensions = data.scoring.dimensions;
|
|
3294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3295
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
3296
|
+
if (seen.has(dimension.name)) {
|
|
3297
|
+
report(
|
|
3298
|
+
["scoring", "dimensions", index, "name"],
|
|
3299
|
+
dimension.name,
|
|
3300
|
+
`"${dimension.name}" is already weighed: a dimension counted twice hides which weight the grade used.`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
seen.add(dimension.name);
|
|
3304
|
+
}
|
|
3305
|
+
if (dimensions.every((dimension) => dimension.weight === 0)) {
|
|
3306
|
+
report(
|
|
3307
|
+
["scoring", "dimensions"],
|
|
3308
|
+
dimensions,
|
|
3309
|
+
"At least one dimension must carry a weight above 0: with every weight at 0 there is no weighted total, and every take would be unscorable."
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (!refused.has("recording")) {
|
|
3314
|
+
const { minSeconds, maxSeconds } = data.recording;
|
|
3315
|
+
if (minSeconds !== void 0 && minSeconds >= maxSeconds) {
|
|
3316
|
+
report(
|
|
3317
|
+
["recording", "minSeconds"],
|
|
3318
|
+
minSeconds,
|
|
3319
|
+
"The shortest take must be shorter than the longest."
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
})
|
|
3324
|
+
);
|
|
3520
3325
|
|
|
3521
3326
|
// src/authoring/read-aloud.ts
|
|
3522
3327
|
var readAloudAuthoring = {
|
|
@@ -3936,6 +3741,330 @@ function checkCriteria(criteria) {
|
|
|
3936
3741
|
return issues;
|
|
3937
3742
|
}
|
|
3938
3743
|
|
|
3744
|
+
// src/schemas/gap-select.ts
|
|
3745
|
+
import { z as z9 } from "zod/v4";
|
|
3746
|
+
var GapSelectChoiceSchema = z9.looseObject({
|
|
3747
|
+
id: z9.string().min(1),
|
|
3748
|
+
text: z9.string().min(1)
|
|
3749
|
+
});
|
|
3750
|
+
var GapSelectBankSchema = z9.looseObject({
|
|
3751
|
+
id: z9.string().min(1),
|
|
3752
|
+
choices: z9.array(GapSelectChoiceSchema).min(2)
|
|
3753
|
+
});
|
|
3754
|
+
var GapSelectGapSchema = z9.looseObject({
|
|
3755
|
+
id: z9.string().min(1),
|
|
3756
|
+
choices: z9.array(GapSelectChoiceSchema).min(2).optional(),
|
|
3757
|
+
bankId: z9.string().min(1).optional(),
|
|
3758
|
+
correctChoiceId: z9.string().min(1),
|
|
3759
|
+
feedback: z9.string().optional()
|
|
3760
|
+
});
|
|
3761
|
+
function resolveChoices2(gap, banks) {
|
|
3762
|
+
if (gap.choices !== void 0) {
|
|
3763
|
+
return gap.bankId === void 0 ? gap.choices : void 0;
|
|
3764
|
+
}
|
|
3765
|
+
if (gap.bankId === void 0) {
|
|
3766
|
+
return void 0;
|
|
3767
|
+
}
|
|
3768
|
+
return banks.find((bank) => bank.id === gap.bankId)?.choices;
|
|
3769
|
+
}
|
|
3770
|
+
function placeholderCounts(passage) {
|
|
3771
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3772
|
+
for (const match of passage.matchAll(PLACEHOLDER_RE)) {
|
|
3773
|
+
const id = match[1];
|
|
3774
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
3775
|
+
}
|
|
3776
|
+
return counts;
|
|
3777
|
+
}
|
|
3778
|
+
var GapSelectDataSchema = z9.looseObject({
|
|
3779
|
+
schemaVersion: z9.literal("1.0"),
|
|
3780
|
+
type: z9.literal("gap-select"),
|
|
3781
|
+
id: z9.string().min(1),
|
|
3782
|
+
title: z9.string().min(1),
|
|
3783
|
+
passage: z9.string().min(1),
|
|
3784
|
+
passageHtml: z9.string().optional(),
|
|
3785
|
+
gaps: z9.array(GapSelectGapSchema).min(1),
|
|
3786
|
+
banks: z9.array(GapSelectBankSchema).optional(),
|
|
3787
|
+
scoringStrategy: z9.enum(["all-or-nothing", "partial"]),
|
|
3788
|
+
presentation: z9.literal("dropdown").optional(),
|
|
3789
|
+
shuffleChoices: z9.boolean().optional(),
|
|
3790
|
+
media: MediaSchema.optional(),
|
|
3791
|
+
feedback: FeedbackSchema.optional(),
|
|
3792
|
+
passThreshold: z9.number().min(0).max(1).optional(),
|
|
3793
|
+
locale: z9.string().optional(),
|
|
3794
|
+
learningObjectives: z9.array(z9.string()).optional(),
|
|
3795
|
+
difficultyLevel: z9.literal([1, 2, 3, 4, 5]).optional(),
|
|
3796
|
+
ai: AiPermissionsSchema.optional()
|
|
3797
|
+
}).refine(
|
|
3798
|
+
(data) => {
|
|
3799
|
+
const ids = (data.banks ?? []).map((bank) => bank.id);
|
|
3800
|
+
return new Set(ids).size === ids.length;
|
|
3801
|
+
},
|
|
3802
|
+
{ error: "Word bank ids must be unique.", path: ["banks"] }
|
|
3803
|
+
).refine(
|
|
3804
|
+
(data) => {
|
|
3805
|
+
const ids = data.gaps.map((gap) => gap.id);
|
|
3806
|
+
return new Set(ids).size === ids.length;
|
|
3807
|
+
},
|
|
3808
|
+
{ error: "Gap ids must be unique.", path: ["gaps"] }
|
|
3809
|
+
).refine(
|
|
3810
|
+
(data) => {
|
|
3811
|
+
const counts = placeholderCounts(data.passage);
|
|
3812
|
+
const ids = data.gaps.map((gap) => gap.id);
|
|
3813
|
+
return counts.size === new Set(ids).size && ids.every((id) => counts.get(id) === 1) && [...counts.values()].every((count) => count === 1);
|
|
3814
|
+
},
|
|
3815
|
+
{
|
|
3816
|
+
error: "Every gap must appear exactly once in the passage as {{gap_id}}, and every {{gap_id}} must have a gap.",
|
|
3817
|
+
path: ["passage"]
|
|
3818
|
+
}
|
|
3819
|
+
).refine(
|
|
3820
|
+
(data) => data.gaps.every((gap) => gap.choices === void 0 !== (gap.bankId === void 0)),
|
|
3821
|
+
{
|
|
3822
|
+
error: "Each gap needs exactly one choice source: its own `choices`, or a `bankId`.",
|
|
3823
|
+
path: ["gaps"]
|
|
3824
|
+
}
|
|
3825
|
+
).refine(
|
|
3826
|
+
(data) => {
|
|
3827
|
+
const bankIds = new Set((data.banks ?? []).map((bank) => bank.id));
|
|
3828
|
+
return data.gaps.every((gap) => gap.bankId === void 0 || bankIds.has(gap.bankId));
|
|
3829
|
+
},
|
|
3830
|
+
{ error: "A gap references a `bankId` that no word bank defines.", path: ["gaps"] }
|
|
3831
|
+
).refine(
|
|
3832
|
+
(data) => {
|
|
3833
|
+
const banks = data.banks ?? [];
|
|
3834
|
+
return data.gaps.every((gap) => {
|
|
3835
|
+
const choices = resolveChoices2(gap, banks);
|
|
3836
|
+
if (choices === void 0) {
|
|
3837
|
+
return true;
|
|
3838
|
+
}
|
|
3839
|
+
const ids = choices.map((choice) => choice.id);
|
|
3840
|
+
return new Set(ids).size === ids.length && ids.includes(gap.correctChoiceId);
|
|
3841
|
+
});
|
|
3842
|
+
},
|
|
3843
|
+
{
|
|
3844
|
+
error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
|
|
3845
|
+
path: ["gaps"]
|
|
3846
|
+
}
|
|
3847
|
+
);
|
|
3848
|
+
|
|
3849
|
+
// src/schemas/redacted.ts
|
|
3850
|
+
import { z as z11 } from "zod/v4";
|
|
3851
|
+
|
|
3852
|
+
// src/schemas/written-response.ts
|
|
3853
|
+
import { z as z10 } from "zod/v4";
|
|
3854
|
+
var WrittenResponseRubricCriterionSchema = z10.looseObject({
|
|
3855
|
+
name: z10.string().min(1),
|
|
3856
|
+
description: z10.string().optional(),
|
|
3857
|
+
weight: z10.number().min(0)
|
|
3858
|
+
});
|
|
3859
|
+
var WrittenResponseRubricSchema = z10.looseObject({
|
|
3860
|
+
label: z10.string().optional(),
|
|
3861
|
+
criteria: z10.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
3862
|
+
});
|
|
3863
|
+
var RedactedWrittenResponseRubricCriterionSchema = z10.strictObject({
|
|
3864
|
+
name: z10.string().min(1),
|
|
3865
|
+
description: z10.string().optional(),
|
|
3866
|
+
weight: z10.number().min(0)
|
|
3867
|
+
});
|
|
3868
|
+
var RedactedWrittenResponseRubricSchema = z10.strictObject({
|
|
3869
|
+
label: z10.string().optional(),
|
|
3870
|
+
criteria: z10.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
|
|
3871
|
+
});
|
|
3872
|
+
var WrittenResponseDataSchema = z10.looseObject({
|
|
3873
|
+
schemaVersion: z10.literal("1.0"),
|
|
3874
|
+
type: z10.literal("written-response"),
|
|
3875
|
+
id: z10.string().min(1),
|
|
3876
|
+
title: z10.string().min(1),
|
|
3877
|
+
prompt: z10.string(),
|
|
3878
|
+
promptHtml: z10.string().optional(),
|
|
3879
|
+
minWords: z10.number().int().min(0),
|
|
3880
|
+
maxWords: z10.number().int().min(1),
|
|
3881
|
+
rubric: WrittenResponseRubricSchema.optional(),
|
|
3882
|
+
languageTarget: z10.string().optional(),
|
|
3883
|
+
media: MediaSchema.optional(),
|
|
3884
|
+
feedback: FeedbackSchema.optional(),
|
|
3885
|
+
passThreshold: z10.number().min(0).max(1).optional(),
|
|
3886
|
+
locale: z10.string().optional(),
|
|
3887
|
+
learningObjectives: z10.array(z10.string()).optional(),
|
|
3888
|
+
difficultyLevel: z10.literal([1, 2, 3, 4, 5]).optional(),
|
|
3889
|
+
ai: AiPermissionsSchema.optional()
|
|
3890
|
+
}).refine((data) => data.maxWords >= data.minWords, {
|
|
3891
|
+
error: "maxWords must be greater than or equal to minWords.",
|
|
3892
|
+
path: ["maxWords"]
|
|
3893
|
+
});
|
|
3894
|
+
|
|
3895
|
+
// src/schemas/redacted.ts
|
|
3896
|
+
var redactedBase = {
|
|
3897
|
+
/** Marker distinguishing a redacted projection from full activity data. */
|
|
3898
|
+
redacted: z11.literal(true),
|
|
3899
|
+
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
3900
|
+
slotKey: z11.string().min(1).optional(),
|
|
3901
|
+
schemaVersion: z11.literal("1.0"),
|
|
3902
|
+
id: z11.string().min(1),
|
|
3903
|
+
title: z11.string().min(1),
|
|
3904
|
+
media: RedactedMediaSchema.optional(),
|
|
3905
|
+
passThreshold: z11.number().min(0).max(1).optional(),
|
|
3906
|
+
locale: z11.string().optional(),
|
|
3907
|
+
learningObjectives: z11.array(z11.string()).optional(),
|
|
3908
|
+
difficultyLevel: z11.literal([1, 2, 3, 4, 5]).optional(),
|
|
3909
|
+
ai: z11.strictObject({ explanations: z11.boolean().optional(), hints: z11.boolean().optional() }).optional()
|
|
3910
|
+
};
|
|
3911
|
+
var RedactedMultipleChoiceOptionMediaSchema = z11.strictObject({
|
|
3912
|
+
type: z11.enum(["image", "audio"]),
|
|
3913
|
+
url: z11.string().min(1),
|
|
3914
|
+
alt: z11.string().min(1).optional(),
|
|
3915
|
+
captionsUrl: z11.string().min(1).optional()
|
|
3916
|
+
});
|
|
3917
|
+
var RedactedMultipleChoiceOptionSchema = z11.strictObject({
|
|
3918
|
+
id: z11.string().min(1),
|
|
3919
|
+
text: z11.string().min(1),
|
|
3920
|
+
media: RedactedMultipleChoiceOptionMediaSchema.optional()
|
|
3921
|
+
});
|
|
3922
|
+
var RedactedMultipleChoiceDataSchema = z11.strictObject({
|
|
3923
|
+
...redactedBase,
|
|
3924
|
+
type: z11.literal("multiple-choice"),
|
|
3925
|
+
question: z11.string().min(1),
|
|
3926
|
+
questionHtml: z11.string().optional(),
|
|
3927
|
+
mode: z11.enum(["single", "multi"]),
|
|
3928
|
+
options: z11.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
|
|
3929
|
+
shuffle: z11.boolean().optional()
|
|
3930
|
+
});
|
|
3931
|
+
var RedactedBlankConfigSchema = z11.strictObject({
|
|
3932
|
+
id: z11.string().min(1),
|
|
3933
|
+
hint: z11.string().optional()
|
|
3934
|
+
});
|
|
3935
|
+
var RedactedFillInTheBlanksDataSchema = z11.strictObject({
|
|
3936
|
+
...redactedBase,
|
|
3937
|
+
type: z11.literal("fill-in-the-blanks"),
|
|
3938
|
+
passage: z11.string().min(1),
|
|
3939
|
+
passageHtml: z11.string().optional(),
|
|
3940
|
+
blanks: z11.array(RedactedBlankConfigSchema).min(1)
|
|
3941
|
+
});
|
|
3942
|
+
var RedactedGapSelectChoiceSchema = z11.strictObject({
|
|
3943
|
+
id: z11.string().min(1),
|
|
3944
|
+
text: z11.string().min(1)
|
|
3945
|
+
});
|
|
3946
|
+
var RedactedGapSelectBankSchema = z11.strictObject({
|
|
3947
|
+
id: z11.string().min(1),
|
|
3948
|
+
choices: z11.array(RedactedGapSelectChoiceSchema).min(2)
|
|
3949
|
+
});
|
|
3950
|
+
var RedactedGapSelectGapSchema = z11.strictObject({
|
|
3951
|
+
id: z11.string().min(1),
|
|
3952
|
+
choices: z11.array(RedactedGapSelectChoiceSchema).min(2).optional(),
|
|
3953
|
+
bankId: z11.string().min(1).optional()
|
|
3954
|
+
});
|
|
3955
|
+
var RedactedGapSelectDataSchema = z11.strictObject({
|
|
3956
|
+
...redactedBase,
|
|
3957
|
+
type: z11.literal("gap-select"),
|
|
3958
|
+
passage: z11.string().min(1),
|
|
3959
|
+
passageHtml: z11.string().optional(),
|
|
3960
|
+
gaps: z11.array(RedactedGapSelectGapSchema).min(1),
|
|
3961
|
+
banks: z11.array(RedactedGapSelectBankSchema).optional(),
|
|
3962
|
+
presentation: z11.literal("dropdown").optional(),
|
|
3963
|
+
shuffleChoices: z11.boolean().optional()
|
|
3964
|
+
});
|
|
3965
|
+
var RedactedWrittenResponseDataSchema = z11.strictObject({
|
|
3966
|
+
...redactedBase,
|
|
3967
|
+
type: z11.literal("written-response"),
|
|
3968
|
+
prompt: z11.string(),
|
|
3969
|
+
promptHtml: z11.string().optional(),
|
|
3970
|
+
minWords: z11.number().int().min(0),
|
|
3971
|
+
maxWords: z11.number().int().min(1),
|
|
3972
|
+
rubric: RedactedWrittenResponseRubricSchema.optional(),
|
|
3973
|
+
languageTarget: z11.string().optional()
|
|
3974
|
+
});
|
|
3975
|
+
var RedactedDictationSlowMediaSchema = z11.strictObject({
|
|
3976
|
+
type: z11.literal("audio"),
|
|
3977
|
+
url: MediaUrlSchema,
|
|
3978
|
+
alt: z11.string().min(1).optional()
|
|
3979
|
+
});
|
|
3980
|
+
var RedactedDictationDataSchema = z11.strictObject({
|
|
3981
|
+
...redactedBase,
|
|
3982
|
+
type: z11.literal("dictation"),
|
|
3983
|
+
slowMedia: RedactedDictationSlowMediaSchema.optional(),
|
|
3984
|
+
hints: z11.strictObject({ mode: z11.literal("progressive-words") }).optional()
|
|
3985
|
+
}).check((ctx) => {
|
|
3986
|
+
const data = ctx.value;
|
|
3987
|
+
for (const captions of captionFieldsOf(data.media)) {
|
|
3988
|
+
ctx.issues.push({
|
|
3989
|
+
code: "custom",
|
|
3990
|
+
input: data.media?.[captions],
|
|
3991
|
+
message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
|
|
3992
|
+
path: ["media", captions]
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
|
|
3996
|
+
ctx.issues.push({
|
|
3997
|
+
code: "custom",
|
|
3998
|
+
input: data.slowMedia,
|
|
3999
|
+
message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
|
|
4000
|
+
path: ["slowMedia"]
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
if (data.slowMedia !== void 0 && data.media === void 0) {
|
|
4004
|
+
ctx.issues.push({
|
|
4005
|
+
code: "custom",
|
|
4006
|
+
input: data.slowMedia,
|
|
4007
|
+
message: "A slow recording accompanies a recording: this payload has no `media`.",
|
|
4008
|
+
path: ["media"]
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
var RedactedReadAloudDataSchema = z11.strictObject({
|
|
4013
|
+
...redactedBase,
|
|
4014
|
+
type: z11.literal("read-aloud"),
|
|
4015
|
+
locale: z11.string().regex(CANONICAL_LOCALE_RE),
|
|
4016
|
+
instructions: z11.string().optional(),
|
|
4017
|
+
referenceText: z11.string().min(1),
|
|
4018
|
+
slowMedia: ReadAloudSlowMediaSchema.optional(),
|
|
4019
|
+
recording: z11.strictObject({
|
|
4020
|
+
maxSeconds: z11.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
|
|
4021
|
+
minSeconds: z11.number().min(0).optional(),
|
|
4022
|
+
maxTakes: z11.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
|
|
4023
|
+
}),
|
|
4024
|
+
scoring: z11.strictObject({
|
|
4025
|
+
dimensions: z11.array(
|
|
4026
|
+
z11.strictObject({
|
|
4027
|
+
name: z11.enum(READ_ALOUD_DIMENSIONS),
|
|
4028
|
+
weight: z11.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
|
|
4029
|
+
})
|
|
4030
|
+
).min(1).max(READ_ALOUD_DIMENSIONS.length)
|
|
4031
|
+
})
|
|
4032
|
+
}).check((ctx) => {
|
|
4033
|
+
const data = ctx.value;
|
|
4034
|
+
if (data.media !== void 0 && data.media.type !== "audio") {
|
|
4035
|
+
ctx.issues.push({
|
|
4036
|
+
code: "custom",
|
|
4037
|
+
input: data.media.type,
|
|
4038
|
+
message: "A read-aloud model recording must be audio, so this payload is not renderable.",
|
|
4039
|
+
path: ["media", "type"]
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
if (data.slowMedia !== void 0 && data.media === void 0) {
|
|
4043
|
+
ctx.issues.push({
|
|
4044
|
+
code: "custom",
|
|
4045
|
+
input: data.slowMedia,
|
|
4046
|
+
message: "A slow model recording accompanies a recording: this payload has no `media`.",
|
|
4047
|
+
path: ["media"]
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
|
|
4051
|
+
ctx.issues.push({
|
|
4052
|
+
code: "custom",
|
|
4053
|
+
input: data.slowMedia,
|
|
4054
|
+
message: "A play budget on `media` cannot coexist with an unbudgeted slow recording of the same content.",
|
|
4055
|
+
path: ["slowMedia"]
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
if (data.slowMedia !== void 0 && data.slowMedia.url === data.media?.url) {
|
|
4059
|
+
ctx.issues.push({
|
|
4060
|
+
code: "custom",
|
|
4061
|
+
input: data.slowMedia.url,
|
|
4062
|
+
message: "The slow model recording must be a different file from the recording.",
|
|
4063
|
+
path: ["slowMedia", "url"]
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
4067
|
+
|
|
3939
4068
|
// src/scoring/activity-scorers/dictation.ts
|
|
3940
4069
|
function scoreDictation(data, response) {
|
|
3941
4070
|
const alignment = alignDictation(data, typeof response?.text === "string" ? response.text : "");
|
|
@@ -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],
|
|
@@ -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],
|
|
@@ -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"],
|
|
@@ -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);
|
|
@@ -4391,17 +4529,10 @@ registerActivityType(dictationType);
|
|
|
4391
4529
|
registerActivityType(readAloudType);
|
|
4392
4530
|
|
|
4393
4531
|
export {
|
|
4394
|
-
|
|
4395
|
-
FeedbackSchema,
|
|
4396
|
-
MediaUrlSchema,
|
|
4397
|
-
NativeControlHintSchema,
|
|
4398
|
-
MediaPlaybackSchema,
|
|
4399
|
-
MediaTrackSchema,
|
|
4532
|
+
maxUnits,
|
|
4400
4533
|
MediaSchema,
|
|
4401
4534
|
RedactedMediaSchema,
|
|
4402
4535
|
PLACEHOLDER_RE,
|
|
4403
|
-
TextMatchPolicySchema,
|
|
4404
|
-
BlankConfigSchema,
|
|
4405
4536
|
FillInTheBlanksDataSchema,
|
|
4406
4537
|
withValidationScope,
|
|
4407
4538
|
DICTATION_MAX_TRANSCRIPT_LENGTH,
|
|
@@ -4419,9 +4550,6 @@ export {
|
|
|
4419
4550
|
UnknownActivityTypeError,
|
|
4420
4551
|
RedactedScoringError,
|
|
4421
4552
|
DeferredScoringError,
|
|
4422
|
-
DictationSlowMediaSchema,
|
|
4423
|
-
DictationEquivalenceSchema,
|
|
4424
|
-
DictationToleranceSchema,
|
|
4425
4553
|
DictationDataSchema,
|
|
4426
4554
|
groupCaptionsField,
|
|
4427
4555
|
GROUP_CAPTIONS_REVEAL_DICTATION,
|
|
@@ -4437,7 +4565,6 @@ export {
|
|
|
4437
4565
|
pathKey,
|
|
4438
4566
|
coveredPathsOf,
|
|
4439
4567
|
MultipleChoiceOptionMediaSchema,
|
|
4440
|
-
MultipleChoiceOptionSchema,
|
|
4441
4568
|
MultipleChoiceDataSchema,
|
|
4442
4569
|
READ_ALOUD_MAX_REFERENCE_LENGTH,
|
|
4443
4570
|
READ_ALOUD_MAX_SECONDS,
|
|
@@ -4446,29 +4573,13 @@ export {
|
|
|
4446
4573
|
SPEECH_ASSESSMENT_MAX_WORDS,
|
|
4447
4574
|
SPEECH_ASSESSMENT_MAX_TEXT_LENGTH,
|
|
4448
4575
|
CANONICAL_LOCALE_RE,
|
|
4449
|
-
ReadAloudSlowMediaSchema,
|
|
4450
4576
|
ReadAloudDataSchema,
|
|
4451
4577
|
countWords,
|
|
4452
|
-
GapSelectChoiceSchema,
|
|
4453
|
-
GapSelectBankSchema,
|
|
4454
|
-
GapSelectGapSchema,
|
|
4455
4578
|
GapSelectDataSchema,
|
|
4456
|
-
|
|
4457
|
-
|
|
4579
|
+
isOwnSchema,
|
|
4580
|
+
readSchema,
|
|
4581
|
+
standardJsonSchemaOf,
|
|
4458
4582
|
WrittenResponseDataSchema,
|
|
4459
|
-
RedactedMultipleChoiceOptionMediaSchema,
|
|
4460
|
-
RedactedMultipleChoiceOptionSchema,
|
|
4461
|
-
RedactedMultipleChoiceDataSchema,
|
|
4462
|
-
RedactedBlankConfigSchema,
|
|
4463
|
-
RedactedFillInTheBlanksDataSchema,
|
|
4464
|
-
RedactedGapSelectChoiceSchema,
|
|
4465
|
-
RedactedGapSelectBankSchema,
|
|
4466
|
-
RedactedGapSelectGapSchema,
|
|
4467
|
-
RedactedGapSelectDataSchema,
|
|
4468
|
-
RedactedWrittenResponseDataSchema,
|
|
4469
|
-
RedactedDictationSlowMediaSchema,
|
|
4470
|
-
RedactedDictationDataSchema,
|
|
4471
|
-
RedactedReadAloudDataSchema,
|
|
4472
4583
|
levenshteinDistance,
|
|
4473
4584
|
matchText,
|
|
4474
4585
|
defineActivityType,
|
|
@@ -4483,4 +4594,4 @@ export {
|
|
|
4483
4594
|
dictationType,
|
|
4484
4595
|
readAloudType
|
|
4485
4596
|
};
|
|
4486
|
-
//# sourceMappingURL=chunk-
|
|
4597
|
+
//# sourceMappingURL=chunk-FBKTION6.js.map
|