@intellectif/lk-core 0.15.0 → 0.17.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 +74 -0
- package/dist/{activity-C7cR8U9n.d.cts → activity-BmjBjXVJ.d.cts} +218 -3
- package/dist/{activity-C7cR8U9n.d.ts → activity-BmjBjXVJ.d.ts} +218 -3
- package/dist/{chunk-W7AQP6AV.cjs → chunk-2CV6NU32.cjs} +24 -22
- package/dist/chunk-2CV6NU32.cjs.map +1 -0
- package/dist/{chunk-DPHX4E7X.js → chunk-D3GP5MET.js} +718 -662
- package/dist/chunk-D3GP5MET.js.map +1 -0
- package/dist/{chunk-QFWEF5ST.cjs → chunk-D4TOPSHV.cjs} +539 -483
- package/dist/chunk-D4TOPSHV.cjs.map +1 -0
- package/dist/{chunk-7ETU6EXQ.cjs → chunk-EOFWSGLU.cjs} +4 -4
- package/dist/{chunk-7ETU6EXQ.cjs.map → chunk-EOFWSGLU.cjs.map} +1 -1
- package/dist/{chunk-L6YBQXSB.js → chunk-WXS5EB65.js} +2 -2
- package/dist/{chunk-54XYQ3CS.js → chunk-X5DAXBJI.js} +2 -2
- package/dist/{chunk-5KWF34LJ.cjs → chunk-XNMKDFPW.cjs} +31 -31
- package/dist/{chunk-5KWF34LJ.cjs.map → chunk-XNMKDFPW.cjs.map} +1 -1
- package/dist/{chunk-EEMVWBPP.js → chunk-Z7QQZ53L.js} +10 -8
- package/dist/chunk-Z7QQZ53L.js.map +1 -0
- package/dist/{index-BWyMDlUS.d.cts → index-CvfeD_cU.d.cts} +144 -86
- package/dist/{index-D10NaztZ.d.ts → index-G_KusUcK.d.ts} +144 -86
- package/dist/index.cjs +544 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -5
- package/dist/index.d.ts +135 -5
- package/dist/index.js +426 -10
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +5 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +4 -2
- 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 +1 -1
- package/dist/chunk-DPHX4E7X.js.map +0 -1
- package/dist/chunk-EEMVWBPP.js.map +0 -1
- package/dist/chunk-QFWEF5ST.cjs.map +0 -1
- package/dist/chunk-W7AQP6AV.cjs.map +0 -1
- /package/dist/{chunk-L6YBQXSB.js.map → chunk-WXS5EB65.js.map} +0 -0
- /package/dist/{chunk-54XYQ3CS.js.map → chunk-X5DAXBJI.js.map} +0 -0
|
@@ -1,42 +1,205 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// src/schemas/ai.ts
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
var AiPermissionsSchema = z.looseObject({
|
|
4
|
+
explanations: z.boolean().optional(),
|
|
5
|
+
hints: z.boolean().optional()
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/schemas/feedback.ts
|
|
9
|
+
import { z as z2 } from "zod/v4";
|
|
10
|
+
var FeedbackSchema = z2.looseObject({
|
|
11
|
+
correct: z2.string().min(1).optional(),
|
|
12
|
+
incorrect: z2.string().min(1).optional()
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// src/schemas/media.ts
|
|
16
|
+
import { z as z3 } from "zod/v4";
|
|
17
|
+
var MediaUrlSchema = z3.union([
|
|
18
|
+
z3.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
19
|
+
error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
|
|
20
|
+
}),
|
|
21
|
+
z3.string().regex(/^\/(?!\/)\S*$/, {
|
|
22
|
+
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
23
|
+
})
|
|
24
|
+
]);
|
|
25
|
+
var NativeControlHintSchema = z3.enum(["hide-download", "hide-rate"]);
|
|
26
|
+
var MediaPlaybackSchema = z3.strictObject({
|
|
27
|
+
/**
|
|
28
|
+
* `native` (the resolved default) renders today's `<audio controls>`
|
|
29
|
+
* unchanged. `minimal` — resolved automatically whenever any enforcement
|
|
30
|
+
* field is set — replaces the browser bar with the SDK transport:
|
|
31
|
+
* play/pause, elapsed/total, mute, volume, optional speed, optional
|
|
32
|
+
* scrubber, and a live plays-remaining status.
|
|
33
|
+
*/
|
|
34
|
+
controls: z3.enum(["native", "minimal"]).optional(),
|
|
35
|
+
/**
|
|
36
|
+
* How many times the recording may be STARTED. A play is consumed when
|
|
37
|
+
* playback begins from anywhere other than where it last stopped, so
|
|
38
|
+
* pausing, resuming, and paging between the questions of one listening
|
|
39
|
+
* group are all free. Enforced in `practice` and `exam`; never in `review`.
|
|
40
|
+
*/
|
|
41
|
+
maxPlays: z3.number().int().min(1).max(20).optional(),
|
|
42
|
+
/** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
|
|
43
|
+
seek: z3.enum(["allow", "none"]).optional(),
|
|
44
|
+
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
45
|
+
rate: z3.enum(["allow", "fixed"]).optional(),
|
|
46
|
+
/** Advisory only. See {@link NativeControlHintSchema}. */
|
|
47
|
+
nativeControlHints: z3.array(NativeControlHintSchema).min(1).max(2).optional()
|
|
48
|
+
});
|
|
49
|
+
var MediaTrackSchema = z3.strictObject({
|
|
50
|
+
kind: z3.enum(["captions", "subtitles"]),
|
|
51
|
+
src: MediaUrlSchema,
|
|
52
|
+
srclang: z3.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
|
|
53
|
+
error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
|
|
54
|
+
}),
|
|
55
|
+
label: z3.string().max(60).refine((label) => label.trim().length > 0, { error: "A track label must not be empty." }),
|
|
56
|
+
default: z3.boolean().optional()
|
|
57
|
+
});
|
|
58
|
+
var MAX_TRACKS = 12;
|
|
59
|
+
function posterFits(m) {
|
|
60
|
+
return m.poster === void 0 || m.type === "video";
|
|
61
|
+
}
|
|
62
|
+
function tracksFit(m) {
|
|
63
|
+
return m.tracks === void 0 || m.type === "audio" || m.type === "video";
|
|
64
|
+
}
|
|
65
|
+
function tracksAreDistinct(m) {
|
|
66
|
+
if (m.tracks === void 0) {
|
|
67
|
+
return true;
|
|
8
68
|
}
|
|
9
|
-
|
|
10
|
-
|
|
69
|
+
const defaults = m.tracks.filter((track) => track.default === true).length;
|
|
70
|
+
const keys = new Set(m.tracks.map((track) => `${track.kind}:${track.srclang.toLowerCase()}`));
|
|
71
|
+
return defaults <= 1 && keys.size === m.tracks.length;
|
|
72
|
+
}
|
|
73
|
+
var POSTER_FITS = { error: "poster is for video media only.", path: ["poster"] };
|
|
74
|
+
var TRACKS_FIT = { error: "tracks are for audio and video media only.", path: ["tracks"] };
|
|
75
|
+
var TRACKS_DISTINCT = {
|
|
76
|
+
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
|
+
path: ["tracks"]
|
|
11
78
|
};
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
79
|
+
var MediaSchema = z3.looseObject({
|
|
80
|
+
type: z3.enum(["image", "audio", "video", "embed"]),
|
|
81
|
+
url: MediaUrlSchema,
|
|
82
|
+
alt: z3.string().min(1).optional(),
|
|
83
|
+
captionsUrl: MediaUrlSchema.optional(),
|
|
84
|
+
tracks: z3.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
85
|
+
poster: MediaUrlSchema.optional(),
|
|
86
|
+
playback: MediaPlaybackSchema.optional()
|
|
87
|
+
}).refine(
|
|
88
|
+
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
89
|
+
{
|
|
90
|
+
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
91
|
+
path: ["alt"]
|
|
17
92
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
93
|
+
).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
|
|
94
|
+
error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
|
|
95
|
+
path: ["url"]
|
|
96
|
+
}).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT).refine((m) => m.playback === void 0 || m.type === "audio", {
|
|
97
|
+
error: "playback policy is supported on audio media only. An embed is a provider iframe the SDK cannot control at all (it has no reliable JS API for a third-party player); an image has nothing to play; video is deliberately deferred, because a video transport must also own fullscreen and Picture-in-Picture and the SDK will not pretend to govern those yet.",
|
|
98
|
+
path: ["playback"]
|
|
99
|
+
}).refine(
|
|
100
|
+
(m) => m.playback?.controls !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
|
|
101
|
+
{
|
|
102
|
+
error: 'controls: "native" cannot carry maxPlays, seek: "none" or rate: "fixed". The browser\'s own bar keeps its play button enabled after a budget is spent, and keeps a scrubber that would move and then silently snap back \u2014 a control that looks operable and does nothing (WCAG 3.2.2 / 4.1.3). Omit `controls` and the SDK transport is used automatically, or use nativeControlHints for a cosmetic hint.',
|
|
103
|
+
path: ["playback", "controls"]
|
|
27
104
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.name = "DeferredScoringError";
|
|
105
|
+
).refine((m) => m.playback?.maxPlays === void 0 || m.playback.seek !== "allow", {
|
|
106
|
+
error: 'maxPlays cannot be combined with seek: "allow". A play is consumed when playback starts from somewhere other than where it stopped, so scrubbing back mid-play would replay the whole recording without spending anything. Omit `seek` \u2014 it resolves to "none" under a budget.',
|
|
107
|
+
path: ["playback", "seek"]
|
|
108
|
+
}).refine(
|
|
109
|
+
(m) => m.playback?.controls !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
|
|
110
|
+
{
|
|
111
|
+
error: 'controls: "minimal" with nothing to enforce trades the browser\'s localized, familiar control bar for the SDK\'s, and buys nothing. Set maxPlays, seek: "none" or rate: "fixed", or omit `controls`.',
|
|
112
|
+
path: ["playback", "controls"]
|
|
37
113
|
}
|
|
38
|
-
|
|
39
|
-
|
|
114
|
+
).refine(
|
|
115
|
+
(m) => m.playback?.nativeControlHints === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
|
|
116
|
+
{
|
|
117
|
+
error: "nativeControlHints only affects the browser's own control bar, and an enforcing policy replaces that bar with the SDK transport \u2014 so the hints would be silently inert. Use them on an otherwise unrestricted recording, or drop them.",
|
|
118
|
+
path: ["playback", "nativeControlHints"]
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
var RedactedMediaSchema = z3.strictObject({
|
|
122
|
+
type: z3.enum(["image", "audio", "video", "embed"]),
|
|
123
|
+
url: MediaUrlSchema,
|
|
124
|
+
alt: z3.string().min(1).optional(),
|
|
125
|
+
captionsUrl: MediaUrlSchema.optional(),
|
|
126
|
+
tracks: z3.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
127
|
+
poster: MediaUrlSchema.optional(),
|
|
128
|
+
playback: MediaPlaybackSchema.optional()
|
|
129
|
+
}).refine(
|
|
130
|
+
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
131
|
+
{
|
|
132
|
+
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
133
|
+
path: ["alt"]
|
|
134
|
+
}
|
|
135
|
+
).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
|
|
136
|
+
error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
|
|
137
|
+
path: ["url"]
|
|
138
|
+
}).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
|
|
139
|
+
|
|
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
|
+
);
|
|
40
203
|
|
|
41
204
|
// src/validation-scope.ts
|
|
42
205
|
var active = null;
|
|
@@ -912,148 +1075,247 @@ function searchReveal(forms, candidate) {
|
|
|
912
1075
|
});
|
|
913
1076
|
}
|
|
914
1077
|
|
|
915
|
-
// src/
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
correct: z.string().min(1).optional(),
|
|
919
|
-
incorrect: z.string().min(1).optional()
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// src/schemas/media.ts
|
|
923
|
-
import { z as z2 } from "zod/v4";
|
|
924
|
-
var MediaUrlSchema = z2.union([
|
|
925
|
-
z2.url().refine((value) => /^(https?|data|blob):/i.test(value), {
|
|
926
|
-
error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
|
|
927
|
-
}),
|
|
928
|
-
z2.string().regex(/^\/(?!\/)\S*$/, {
|
|
929
|
-
error: 'Relative media URLs must be root-relative (a single leading "/").'
|
|
930
|
-
})
|
|
931
|
-
]);
|
|
932
|
-
var NativeControlHintSchema = z2.enum(["hide-download", "hide-rate"]);
|
|
933
|
-
var MediaPlaybackSchema = z2.strictObject({
|
|
934
|
-
/**
|
|
935
|
-
* `native` (the resolved default) renders today's `<audio controls>`
|
|
936
|
-
* unchanged. `minimal` — resolved automatically whenever any enforcement
|
|
937
|
-
* field is set — replaces the browser bar with the SDK transport:
|
|
938
|
-
* play/pause, elapsed/total, mute, volume, optional speed, optional
|
|
939
|
-
* scrubber, and a live plays-remaining status.
|
|
940
|
-
*/
|
|
941
|
-
controls: z2.enum(["native", "minimal"]).optional(),
|
|
942
|
-
/**
|
|
943
|
-
* How many times the recording may be STARTED. A play is consumed when
|
|
944
|
-
* playback begins from anywhere other than where it last stopped, so
|
|
945
|
-
* pausing, resuming, and paging between the questions of one listening
|
|
946
|
-
* group are all free. Enforced in `practice` and `exam`; never in `review`.
|
|
947
|
-
*/
|
|
948
|
-
maxPlays: z2.number().int().min(1).max(20).optional(),
|
|
949
|
-
/** `none` renders no scrubber and reverts an out-of-band seek to the high-water mark. */
|
|
950
|
-
seek: z2.enum(["allow", "none"]).optional(),
|
|
951
|
-
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
952
|
-
rate: z2.enum(["allow", "fixed"]).optional(),
|
|
953
|
-
/** Advisory only. See {@link NativeControlHintSchema}. */
|
|
954
|
-
nativeControlHints: z2.array(NativeControlHintSchema).min(1).max(2).optional()
|
|
955
|
-
});
|
|
956
|
-
var MediaTrackSchema = z2.strictObject({
|
|
957
|
-
kind: z2.enum(["captions", "subtitles"]),
|
|
958
|
-
src: MediaUrlSchema,
|
|
959
|
-
srclang: z2.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, {
|
|
960
|
-
error: 'srclang must be a BCP 47 language tag, such as "en", "es" or "pt-BR".'
|
|
961
|
-
}),
|
|
962
|
-
label: z2.string().max(60).refine((label) => label.trim().length > 0, { error: "A track label must not be empty." }),
|
|
963
|
-
default: z2.boolean().optional()
|
|
964
|
-
});
|
|
965
|
-
var MAX_TRACKS = 12;
|
|
966
|
-
function posterFits(m) {
|
|
967
|
-
return m.poster === void 0 || m.type === "video";
|
|
968
|
-
}
|
|
969
|
-
function tracksFit(m) {
|
|
970
|
-
return m.tracks === void 0 || m.type === "audio" || m.type === "video";
|
|
1078
|
+
// src/scoring/dictation/edit-distance.ts
|
|
1079
|
+
function codePoints(text) {
|
|
1080
|
+
return Array.from(text, (character) => character.codePointAt(0));
|
|
971
1081
|
}
|
|
972
|
-
function
|
|
973
|
-
if (
|
|
974
|
-
return
|
|
1082
|
+
function editDistance(a, b) {
|
|
1083
|
+
if (a === b) {
|
|
1084
|
+
return 0;
|
|
975
1085
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
var POSTER_FITS = { error: "poster is for video media only.", path: ["poster"] };
|
|
981
|
-
var TRACKS_FIT = { error: "tracks are for audio and video media only.", path: ["tracks"] };
|
|
982
|
-
var TRACKS_DISTINCT = {
|
|
983
|
-
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.",
|
|
984
|
-
path: ["tracks"]
|
|
985
|
-
};
|
|
986
|
-
var MediaSchema = z2.looseObject({
|
|
987
|
-
type: z2.enum(["image", "audio", "video", "embed"]),
|
|
988
|
-
url: MediaUrlSchema,
|
|
989
|
-
alt: z2.string().min(1).optional(),
|
|
990
|
-
captionsUrl: MediaUrlSchema.optional(),
|
|
991
|
-
tracks: z2.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
|
|
992
|
-
poster: MediaUrlSchema.optional(),
|
|
993
|
-
playback: MediaPlaybackSchema.optional()
|
|
994
|
-
}).refine(
|
|
995
|
-
(m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
|
|
996
|
-
{
|
|
997
|
-
error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
|
|
998
|
-
path: ["alt"]
|
|
1086
|
+
let left = codePoints(a);
|
|
1087
|
+
let right = codePoints(b);
|
|
1088
|
+
if (left.length < right.length) {
|
|
1089
|
+
[left, right] = [right, left];
|
|
999
1090
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
path: ["url"]
|
|
1003
|
-
}).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT).refine((m) => m.playback === void 0 || m.type === "audio", {
|
|
1004
|
-
error: "playback policy is supported on audio media only. An embed is a provider iframe the SDK cannot control at all (it has no reliable JS API for a third-party player); an image has nothing to play; video is deliberately deferred, because a video transport must also own fullscreen and Picture-in-Picture and the SDK will not pretend to govern those yet.",
|
|
1005
|
-
path: ["playback"]
|
|
1006
|
-
}).refine(
|
|
1007
|
-
(m) => m.playback?.controls !== "native" || m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
|
|
1008
|
-
{
|
|
1009
|
-
error: 'controls: "native" cannot carry maxPlays, seek: "none" or rate: "fixed". The browser\'s own bar keeps its play button enabled after a budget is spent, and keeps a scrubber that would move and then silently snap back \u2014 a control that looks operable and does nothing (WCAG 3.2.2 / 4.1.3). Omit `controls` and the SDK transport is used automatically, or use nativeControlHints for a cosmetic hint.',
|
|
1010
|
-
path: ["playback", "controls"]
|
|
1091
|
+
if (right.length === 0) {
|
|
1092
|
+
return left.length;
|
|
1011
1093
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
(m) => m.playback?.controls !== "minimal" || m.playback.maxPlays !== void 0 || m.playback.seek === "none" || m.playback.rate === "fixed",
|
|
1017
|
-
{
|
|
1018
|
-
error: 'controls: "minimal" with nothing to enforce trades the browser\'s localized, familiar control bar for the SDK\'s, and buys nothing. Set maxPlays, seek: "none" or rate: "fixed", or omit `controls`.',
|
|
1019
|
-
path: ["playback", "controls"]
|
|
1094
|
+
let previous = new Uint32Array(right.length + 1);
|
|
1095
|
+
let current = new Uint32Array(right.length + 1);
|
|
1096
|
+
for (let j = 0; j <= right.length; j += 1) {
|
|
1097
|
+
previous[j] = j;
|
|
1020
1098
|
}
|
|
1021
|
-
)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1099
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
1100
|
+
current[0] = i;
|
|
1101
|
+
const leftPoint = left[i - 1];
|
|
1102
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
1103
|
+
const deletion = previous[j] + 1;
|
|
1104
|
+
const insertion = current[j - 1] + 1;
|
|
1105
|
+
const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
|
|
1106
|
+
current[j] = Math.min(deletion, insertion, substitution);
|
|
1107
|
+
}
|
|
1108
|
+
[previous, current] = [current, previous];
|
|
1026
1109
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1110
|
+
return previous[right.length];
|
|
1111
|
+
}
|
|
1112
|
+
function similarity(a, b) {
|
|
1113
|
+
const max = Math.max(codePoints(a).length, codePoints(b).length);
|
|
1114
|
+
return max === 0 ? 0 : (max - editDistance(a, b)) / max;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/scoring/dictation/align.ts
|
|
1118
|
+
function dictationReferenceWords(data) {
|
|
1119
|
+
return rawCandidates(data).map(
|
|
1120
|
+
(candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
|
|
1121
|
+
itemId: `w${index + 1}`,
|
|
1122
|
+
word
|
|
1123
|
+
}))
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
function alignDictation(data, text) {
|
|
1127
|
+
const raw = typeof text === "string" ? text : "";
|
|
1128
|
+
const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
|
|
1129
|
+
const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
|
|
1130
|
+
const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
|
|
1131
|
+
const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
|
|
1132
|
+
const candidates = [];
|
|
1133
|
+
rawCandidates(data).forEach((candidate, index) => {
|
|
1134
|
+
const candidateText = normalizeCandidate(candidate, data);
|
|
1135
|
+
if (candidateText !== "") {
|
|
1136
|
+
candidates.push({ index, text: candidateText });
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
if (candidates.length === 0) {
|
|
1140
|
+
return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
|
|
1041
1141
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1142
|
+
let chosen = candidates[0];
|
|
1143
|
+
let best = similarity(chosen.text, attempt);
|
|
1144
|
+
for (const candidate of candidates.slice(1)) {
|
|
1145
|
+
const candidateSimilarity = similarity(candidate.text, attempt);
|
|
1146
|
+
if (candidateSimilarity > best) {
|
|
1147
|
+
best = candidateSimilarity;
|
|
1148
|
+
chosen = candidate;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
candidateIndex: chosen.index,
|
|
1153
|
+
reference: chosen.text,
|
|
1154
|
+
attempt,
|
|
1155
|
+
truncated,
|
|
1156
|
+
similarity: best,
|
|
1157
|
+
words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function rawCandidates(data) {
|
|
1161
|
+
return [
|
|
1162
|
+
data.transcript,
|
|
1163
|
+
...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
|
|
1164
|
+
];
|
|
1165
|
+
}
|
|
1166
|
+
function normalizeCandidate(candidate, data) {
|
|
1167
|
+
return typeof candidate === "string" ? cutAt(
|
|
1168
|
+
normalizeDictationText(
|
|
1169
|
+
truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
1170
|
+
data.tolerance
|
|
1171
|
+
),
|
|
1172
|
+
DICTATION_MAX_TRANSCRIPT_LENGTH
|
|
1173
|
+
) : "";
|
|
1174
|
+
}
|
|
1175
|
+
function cutAt(normalized, max) {
|
|
1176
|
+
const cut = truncateCodePoints(normalized, max);
|
|
1177
|
+
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
1178
|
+
}
|
|
1179
|
+
function tokensOf(normalized) {
|
|
1180
|
+
return normalized === "" ? [] : normalized.split(" ");
|
|
1181
|
+
}
|
|
1182
|
+
var STEP = {
|
|
1183
|
+
/** A reference element paired with an attempt element — equal or substituted. */
|
|
1184
|
+
pair: 0,
|
|
1185
|
+
/** A reference element the attempt lacks. */
|
|
1186
|
+
missing: 1,
|
|
1187
|
+
/** An attempt element the reference lacks. */
|
|
1188
|
+
extra: 2
|
|
1189
|
+
};
|
|
1190
|
+
function alignSequences(reference, attempt) {
|
|
1191
|
+
const rows = reference.length;
|
|
1192
|
+
const columns = attempt.length;
|
|
1193
|
+
const width = columns + 1;
|
|
1194
|
+
const steps = new Uint8Array((rows + 1) * width);
|
|
1195
|
+
let previous = new Uint32Array(width);
|
|
1196
|
+
let current = new Uint32Array(width);
|
|
1197
|
+
for (let j2 = 0; j2 <= columns; j2 += 1) {
|
|
1198
|
+
previous[j2] = j2;
|
|
1199
|
+
steps[j2] = STEP.extra;
|
|
1200
|
+
}
|
|
1201
|
+
for (let i2 = 1; i2 <= rows; i2 += 1) {
|
|
1202
|
+
current[0] = i2;
|
|
1203
|
+
steps[i2 * width] = STEP.missing;
|
|
1204
|
+
const referenceItem = reference[i2 - 1];
|
|
1205
|
+
for (let j2 = 1; j2 <= columns; j2 += 1) {
|
|
1206
|
+
const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
|
|
1207
|
+
const missing = previous[j2] + 1;
|
|
1208
|
+
const extra = current[j2 - 1] + 1;
|
|
1209
|
+
const cost = Math.min(paired, missing, extra);
|
|
1210
|
+
current[j2] = cost;
|
|
1211
|
+
steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
|
|
1212
|
+
}
|
|
1213
|
+
[previous, current] = [current, previous];
|
|
1214
|
+
}
|
|
1215
|
+
const path = [];
|
|
1216
|
+
let i = rows;
|
|
1217
|
+
let j = columns;
|
|
1218
|
+
while (i > 0 || j > 0) {
|
|
1219
|
+
const step = steps[i * width + j];
|
|
1220
|
+
if (step === STEP.pair) {
|
|
1221
|
+
i -= 1;
|
|
1222
|
+
j -= 1;
|
|
1223
|
+
} else if (step === STEP.missing) {
|
|
1224
|
+
i -= 1;
|
|
1225
|
+
} else {
|
|
1226
|
+
j -= 1;
|
|
1227
|
+
}
|
|
1228
|
+
path.push({ step, referenceIndex: i, attemptIndex: j });
|
|
1229
|
+
}
|
|
1230
|
+
return path.reverse();
|
|
1231
|
+
}
|
|
1232
|
+
function alignWords(reference, attempt) {
|
|
1233
|
+
const words = [];
|
|
1234
|
+
let ordinal = 0;
|
|
1235
|
+
for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
|
|
1236
|
+
if (step === STEP.extra) {
|
|
1237
|
+
words.push({
|
|
1238
|
+
reference: "",
|
|
1239
|
+
attempt: attempt[attemptIndex],
|
|
1240
|
+
similarity: 0,
|
|
1241
|
+
status: "extra"
|
|
1242
|
+
});
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
ordinal += 1;
|
|
1246
|
+
const referenceWord = reference[referenceIndex];
|
|
1247
|
+
if (step === STEP.missing) {
|
|
1248
|
+
words.push({
|
|
1249
|
+
itemId: `w${ordinal}`,
|
|
1250
|
+
reference: referenceWord,
|
|
1251
|
+
attempt: "",
|
|
1252
|
+
similarity: 0,
|
|
1253
|
+
status: "missing"
|
|
1254
|
+
});
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const attemptWord = attempt[attemptIndex];
|
|
1258
|
+
words.push({
|
|
1259
|
+
itemId: `w${ordinal}`,
|
|
1260
|
+
reference: referenceWord,
|
|
1261
|
+
attempt: attemptWord,
|
|
1262
|
+
similarity: similarity(referenceWord, attemptWord),
|
|
1263
|
+
status: referenceWord === attemptWord ? "correct" : "incorrect"
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
return words;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/errors.ts
|
|
1270
|
+
var ActivitySchemaError = class extends Error {
|
|
1271
|
+
constructor(activityType, errors) {
|
|
1272
|
+
super(`Invalid activity data for type "${activityType}"`);
|
|
1273
|
+
this.activityType = activityType;
|
|
1274
|
+
this.errors = errors;
|
|
1275
|
+
this.name = "ActivitySchemaError";
|
|
1276
|
+
}
|
|
1277
|
+
activityType;
|
|
1278
|
+
errors;
|
|
1279
|
+
};
|
|
1280
|
+
var UnknownActivityTypeError = class extends Error {
|
|
1281
|
+
constructor(activityType) {
|
|
1282
|
+
super(`Activity type "${activityType}" is not registered`);
|
|
1283
|
+
this.activityType = activityType;
|
|
1284
|
+
this.name = "UnknownActivityTypeError";
|
|
1285
|
+
}
|
|
1286
|
+
activityType;
|
|
1287
|
+
};
|
|
1288
|
+
var RedactedScoringError = class extends Error {
|
|
1289
|
+
constructor(activityType) {
|
|
1290
|
+
super(
|
|
1291
|
+
`Activity data for "${activityType}" carries no answer key (it looks redacted), so it cannot be scored. Score against the full activity data server-side. evaluate() answers this input without throwing, but it never grades it.`
|
|
1292
|
+
);
|
|
1293
|
+
this.activityType = activityType;
|
|
1294
|
+
this.name = "RedactedScoringError";
|
|
1295
|
+
}
|
|
1296
|
+
activityType;
|
|
1297
|
+
};
|
|
1298
|
+
var DeferredScoringError = class extends Error {
|
|
1299
|
+
constructor(activityType) {
|
|
1300
|
+
super(
|
|
1301
|
+
`Activity type "${activityType}" is graded asynchronously and has no synchronous score. Use evaluate() \u2014 it returns { status: 'deferred' } for this type.`
|
|
1302
|
+
);
|
|
1303
|
+
this.activityType = activityType;
|
|
1304
|
+
this.name = "DeferredScoringError";
|
|
1305
|
+
}
|
|
1306
|
+
activityType;
|
|
1307
|
+
};
|
|
1046
1308
|
|
|
1047
1309
|
// src/schemas/dictation.ts
|
|
1048
|
-
import { z as
|
|
1049
|
-
var DictationSlowMediaSchema =
|
|
1050
|
-
type:
|
|
1310
|
+
import { z as z5 } from "zod/v4";
|
|
1311
|
+
var DictationSlowMediaSchema = z5.looseObject({
|
|
1312
|
+
type: z5.literal("audio"),
|
|
1051
1313
|
url: MediaUrlSchema,
|
|
1052
|
-
alt:
|
|
1314
|
+
alt: z5.string().min(1).optional()
|
|
1053
1315
|
});
|
|
1054
|
-
var RuleTextSchema =
|
|
1055
|
-
var TranscriptTextSchema =
|
|
1056
|
-
var DictationEquivalenceSchema =
|
|
1316
|
+
var RuleTextSchema = z5.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
|
|
1317
|
+
var TranscriptTextSchema = z5.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
|
|
1318
|
+
var DictationEquivalenceSchema = z5.looseObject({
|
|
1057
1319
|
from: RuleTextSchema,
|
|
1058
1320
|
to: RuleTextSchema
|
|
1059
1321
|
}).refine((rule) => codePointLength(rule.from) <= DICTATION_MAX_EQUIVALENCE_LENGTH, {
|
|
@@ -1063,8 +1325,8 @@ var DictationEquivalenceSchema = z3.looseObject({
|
|
|
1063
1325
|
error: `A rewrite is at most ${DICTATION_MAX_EQUIVALENCE_LENGTH} characters.`,
|
|
1064
1326
|
path: ["to"]
|
|
1065
1327
|
});
|
|
1066
|
-
var DictationToleranceSchema =
|
|
1067
|
-
equivalences:
|
|
1328
|
+
var DictationToleranceSchema = z5.looseObject({
|
|
1329
|
+
equivalences: z5.array(DictationEquivalenceSchema).max(DICTATION_MAX_EQUIVALENCES, {
|
|
1068
1330
|
error: `A dictation carries at most ${DICTATION_MAX_EQUIVALENCES} equivalence rules.`
|
|
1069
1331
|
}).optional()
|
|
1070
1332
|
});
|
|
@@ -1087,26 +1349,27 @@ function isEmpty(measured) {
|
|
|
1087
1349
|
return measured.normalized === "";
|
|
1088
1350
|
}
|
|
1089
1351
|
function checkEvenAfterIssues(check) {
|
|
1090
|
-
const guard = new
|
|
1352
|
+
const guard = new z5.core.$ZodCheck({ check: "custom", when: () => true });
|
|
1091
1353
|
guard._zod.check = check;
|
|
1092
1354
|
return guard;
|
|
1093
1355
|
}
|
|
1094
|
-
var DictationDataShape =
|
|
1095
|
-
schemaVersion:
|
|
1096
|
-
type:
|
|
1097
|
-
id:
|
|
1098
|
-
title:
|
|
1356
|
+
var DictationDataShape = z5.looseObject({
|
|
1357
|
+
schemaVersion: z5.literal("1.0"),
|
|
1358
|
+
type: z5.literal("dictation"),
|
|
1359
|
+
id: z5.string().min(1),
|
|
1360
|
+
title: z5.string().min(1),
|
|
1099
1361
|
transcript: TranscriptTextSchema,
|
|
1100
|
-
acceptedTranscripts:
|
|
1362
|
+
acceptedTranscripts: z5.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
|
|
1101
1363
|
media: MediaSchema.optional(),
|
|
1102
1364
|
slowMedia: DictationSlowMediaSchema.optional(),
|
|
1103
|
-
hints:
|
|
1365
|
+
hints: z5.looseObject({ mode: z5.literal("progressive-words") }).optional(),
|
|
1104
1366
|
tolerance: DictationToleranceSchema.optional(),
|
|
1105
1367
|
feedback: FeedbackSchema.optional(),
|
|
1106
|
-
passThreshold:
|
|
1107
|
-
locale:
|
|
1108
|
-
learningObjectives:
|
|
1109
|
-
difficultyLevel:
|
|
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()
|
|
1110
1373
|
});
|
|
1111
1374
|
var DictationDataSchema = DictationDataShape.check(
|
|
1112
1375
|
checkEvenAfterIssues((ctx) => {
|
|
@@ -1208,10 +1471,10 @@ var DictationDataSchema = DictationDataShape.check(
|
|
|
1208
1471
|
}
|
|
1209
1472
|
for (const field of ["media", "slowMedia"]) {
|
|
1210
1473
|
const recording = data[field];
|
|
1211
|
-
|
|
1474
|
+
for (const captions of captionFieldsOf(recording)) {
|
|
1212
1475
|
report(
|
|
1213
|
-
[field,
|
|
1214
|
-
recording
|
|
1476
|
+
[field, captions],
|
|
1477
|
+
recording?.[captions],
|
|
1215
1478
|
"A dictation recording cannot carry captions: the captions are the answer. Offer an accessible alternative as a different item."
|
|
1216
1479
|
);
|
|
1217
1480
|
}
|
|
@@ -1247,92 +1510,44 @@ var DictationDataSchema = DictationDataShape.check(
|
|
|
1247
1510
|
}
|
|
1248
1511
|
})
|
|
1249
1512
|
);
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
if (!isObject(
|
|
1253
|
-
return
|
|
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 [];
|
|
1254
1517
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
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;
|
|
1257
1534
|
}
|
|
1258
|
-
|
|
1535
|
+
const played = group.items.some(
|
|
1259
1536
|
(item) => isObject(item) && item.type === "dictation" && item.media === void 0
|
|
1260
1537
|
);
|
|
1538
|
+
return played ? field : void 0;
|
|
1261
1539
|
}
|
|
1262
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.";
|
|
1263
1541
|
|
|
1264
|
-
// src/schemas/fill-in-the-blanks.ts
|
|
1265
|
-
import { z as z4 } from "zod/v4";
|
|
1266
|
-
var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
1267
|
-
var TextMatchPolicySchema = z4.looseObject({
|
|
1268
|
-
caseSensitive: z4.boolean().optional(),
|
|
1269
|
-
trim: z4.boolean().optional(),
|
|
1270
|
-
normalize: z4.enum(["none", "NFC", "NFKC"]).optional(),
|
|
1271
|
-
foldDiacritics: z4.boolean().optional(),
|
|
1272
|
-
collapseInnerWhitespace: z4.boolean().optional(),
|
|
1273
|
-
ignorePunctuation: z4.boolean().optional(),
|
|
1274
|
-
levenshtein: z4.number().int().min(0).optional(),
|
|
1275
|
-
locale: z4.string().optional()
|
|
1276
|
-
});
|
|
1277
|
-
var BlankConfigSchema = z4.looseObject({
|
|
1278
|
-
id: z4.string().min(1),
|
|
1279
|
-
acceptedAnswers: z4.array(
|
|
1280
|
-
z4.string().min(1).refine((answer) => answer.trim().length > 0, {
|
|
1281
|
-
error: "Accepted answers must contain non-whitespace characters."
|
|
1282
|
-
})
|
|
1283
|
-
).min(1),
|
|
1284
|
-
caseSensitive: z4.boolean().optional(),
|
|
1285
|
-
trimWhitespace: z4.boolean().optional(),
|
|
1286
|
-
match: TextMatchPolicySchema.optional(),
|
|
1287
|
-
hint: z4.string().optional(),
|
|
1288
|
-
feedback: z4.string().optional()
|
|
1289
|
-
});
|
|
1290
|
-
var FillInTheBlanksDataSchema = z4.looseObject({
|
|
1291
|
-
schemaVersion: z4.literal("1.0"),
|
|
1292
|
-
type: z4.literal("fill-in-the-blanks"),
|
|
1293
|
-
id: z4.string().min(1),
|
|
1294
|
-
title: z4.string().min(1),
|
|
1295
|
-
passage: z4.string().min(1),
|
|
1296
|
-
passageHtml: z4.string().optional(),
|
|
1297
|
-
blanks: z4.array(BlankConfigSchema).min(1),
|
|
1298
|
-
scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
|
|
1299
|
-
media: MediaSchema.optional(),
|
|
1300
|
-
feedback: FeedbackSchema.optional(),
|
|
1301
|
-
passThreshold: z4.number().min(0).max(1).optional(),
|
|
1302
|
-
locale: z4.string().optional(),
|
|
1303
|
-
learningObjectives: z4.array(z4.string()).optional(),
|
|
1304
|
-
difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
|
|
1305
|
-
}).refine(
|
|
1306
|
-
(data) => {
|
|
1307
|
-
const placeholderCounts2 = /* @__PURE__ */ new Map();
|
|
1308
|
-
for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
|
|
1309
|
-
const id = match[1];
|
|
1310
|
-
placeholderCounts2.set(id, (placeholderCounts2.get(id) ?? 0) + 1);
|
|
1311
|
-
}
|
|
1312
|
-
const blankIds = data.blanks.map((blank) => blank.id);
|
|
1313
|
-
if (new Set(blankIds).size !== blankIds.length) {
|
|
1314
|
-
return false;
|
|
1315
|
-
}
|
|
1316
|
-
if (placeholderCounts2.size !== blankIds.length) {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
return blankIds.every((id) => placeholderCounts2.get(id) === 1);
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
|
|
1323
|
-
path: ["passage"]
|
|
1324
|
-
}
|
|
1325
|
-
);
|
|
1326
|
-
|
|
1327
1542
|
// src/schemas/multiple-choice.ts
|
|
1328
|
-
import { z as
|
|
1329
|
-
var MultipleChoiceOptionMediaSchema =
|
|
1543
|
+
import { z as z6 } from "zod/v4";
|
|
1544
|
+
var MultipleChoiceOptionMediaSchema = z6.looseObject({
|
|
1330
1545
|
// No `embed`: an iframe swallows the click that selects the option, so the
|
|
1331
1546
|
// learner could not choose it. No `video`: a native control bar inside the
|
|
1332
1547
|
// option's label eats the same click.
|
|
1333
|
-
type:
|
|
1548
|
+
type: z6.enum(["image", "audio"]),
|
|
1334
1549
|
url: MediaUrlSchema,
|
|
1335
|
-
alt:
|
|
1550
|
+
alt: z6.string().min(1).optional(),
|
|
1336
1551
|
captionsUrl: MediaUrlSchema.optional()
|
|
1337
1552
|
}).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
|
|
1338
1553
|
error: "An image option requires a non-empty `alt`.",
|
|
@@ -1341,30 +1556,31 @@ var MultipleChoiceOptionMediaSchema = z5.looseObject({
|
|
|
1341
1556
|
error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
|
|
1342
1557
|
path: ["playback"]
|
|
1343
1558
|
});
|
|
1344
|
-
var MultipleChoiceOptionSchema =
|
|
1345
|
-
id:
|
|
1346
|
-
text:
|
|
1347
|
-
isCorrect:
|
|
1348
|
-
feedback:
|
|
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(),
|
|
1349
1564
|
media: MultipleChoiceOptionMediaSchema.optional()
|
|
1350
1565
|
});
|
|
1351
|
-
var MultipleChoiceDataSchema =
|
|
1352
|
-
schemaVersion:
|
|
1353
|
-
type:
|
|
1354
|
-
id:
|
|
1355
|
-
title:
|
|
1356
|
-
question:
|
|
1357
|
-
questionHtml:
|
|
1358
|
-
mode:
|
|
1359
|
-
options:
|
|
1360
|
-
scoringStrategy:
|
|
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"]),
|
|
1361
1576
|
media: MediaSchema.optional(),
|
|
1362
1577
|
feedback: FeedbackSchema.optional(),
|
|
1363
|
-
passThreshold:
|
|
1364
|
-
shuffle:
|
|
1365
|
-
locale:
|
|
1366
|
-
learningObjectives:
|
|
1367
|
-
difficultyLevel:
|
|
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()
|
|
1368
1584
|
}).refine((data) => data.options.some((option) => option.isCorrect), {
|
|
1369
1585
|
error: "At least one option must be marked correct.",
|
|
1370
1586
|
path: ["options"]
|
|
@@ -1388,7 +1604,7 @@ var SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
|
|
|
1388
1604
|
var SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
|
|
1389
1605
|
|
|
1390
1606
|
// src/schemas/read-aloud.ts
|
|
1391
|
-
import { z as
|
|
1607
|
+
import { z as z7 } from "zod/v4";
|
|
1392
1608
|
|
|
1393
1609
|
// src/scoring/speech/locale.ts
|
|
1394
1610
|
var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
|
|
@@ -1396,49 +1612,50 @@ var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$
|
|
|
1396
1612
|
// src/schemas/read-aloud.ts
|
|
1397
1613
|
var READ_ALOUD_DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
|
|
1398
1614
|
var READ_ALOUD_MAX_DIMENSIONS = READ_ALOUD_DIMENSIONS.length;
|
|
1399
|
-
var ReadAloudSlowMediaSchema =
|
|
1400
|
-
type:
|
|
1615
|
+
var ReadAloudSlowMediaSchema = z7.strictObject({
|
|
1616
|
+
type: z7.literal("audio"),
|
|
1401
1617
|
url: MediaUrlSchema,
|
|
1402
|
-
alt:
|
|
1618
|
+
alt: z7.string().min(1).optional()
|
|
1403
1619
|
});
|
|
1404
|
-
var ReferenceTextSchema =
|
|
1620
|
+
var ReferenceTextSchema = z7.string().min(1).refine((text) => text.trim() !== "", {
|
|
1405
1621
|
error: "The text to read aloud must contain something to read."
|
|
1406
1622
|
}).meta({ maxLength: READ_ALOUD_MAX_REFERENCE_LENGTH });
|
|
1407
|
-
var RecordingBoundsSchema =
|
|
1408
|
-
maxSeconds:
|
|
1409
|
-
minSeconds:
|
|
1410
|
-
maxTakes:
|
|
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()
|
|
1411
1627
|
});
|
|
1412
|
-
var ReadAloudDimensionWeightSchema =
|
|
1413
|
-
name:
|
|
1414
|
-
weight:
|
|
1628
|
+
var ReadAloudDimensionWeightSchema = z7.looseObject({
|
|
1629
|
+
name: z7.enum(READ_ALOUD_DIMENSIONS),
|
|
1630
|
+
weight: z7.number().min(0).max(READ_ALOUD_MAX_DIMENSION_WEIGHT)
|
|
1415
1631
|
});
|
|
1416
|
-
var ReadAloudScoringSchema =
|
|
1417
|
-
dimensions:
|
|
1632
|
+
var ReadAloudScoringSchema = z7.looseObject({
|
|
1633
|
+
dimensions: z7.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
|
|
1418
1634
|
});
|
|
1419
1635
|
function checkEvenAfterIssues2(check) {
|
|
1420
|
-
const guard = new
|
|
1636
|
+
const guard = new z7.core.$ZodCheck({ check: "custom", when: () => true });
|
|
1421
1637
|
guard._zod.check = check;
|
|
1422
1638
|
return guard;
|
|
1423
1639
|
}
|
|
1424
|
-
var ReadAloudDataShape =
|
|
1425
|
-
schemaVersion:
|
|
1426
|
-
type:
|
|
1427
|
-
id:
|
|
1428
|
-
title:
|
|
1429
|
-
instructions:
|
|
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(),
|
|
1430
1646
|
referenceText: ReferenceTextSchema,
|
|
1431
|
-
locale:
|
|
1647
|
+
locale: z7.string().regex(CANONICAL_LOCALE_RE, {
|
|
1432
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.`
|
|
1433
1649
|
}),
|
|
1434
1650
|
media: MediaSchema.optional(),
|
|
1435
1651
|
slowMedia: ReadAloudSlowMediaSchema.optional(),
|
|
1436
1652
|
recording: RecordingBoundsSchema,
|
|
1437
1653
|
scoring: ReadAloudScoringSchema,
|
|
1438
|
-
passThreshold:
|
|
1654
|
+
passThreshold: z7.number().min(0).max(1).optional(),
|
|
1439
1655
|
feedback: FeedbackSchema.optional(),
|
|
1440
|
-
learningObjectives:
|
|
1441
|
-
difficultyLevel:
|
|
1656
|
+
learningObjectives: z7.array(z7.string()).optional(),
|
|
1657
|
+
difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional(),
|
|
1658
|
+
ai: AiPermissionsSchema.optional()
|
|
1442
1659
|
});
|
|
1443
1660
|
var ReadAloudDataSchema = ReadAloudDataShape.check(
|
|
1444
1661
|
checkEvenAfterIssues2((ctx) => {
|
|
@@ -1554,21 +1771,21 @@ function countWords(text) {
|
|
|
1554
1771
|
}
|
|
1555
1772
|
|
|
1556
1773
|
// src/schemas/gap-select.ts
|
|
1557
|
-
import { z as
|
|
1558
|
-
var GapSelectChoiceSchema =
|
|
1559
|
-
id:
|
|
1560
|
-
text:
|
|
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)
|
|
1561
1778
|
});
|
|
1562
|
-
var GapSelectBankSchema =
|
|
1563
|
-
id:
|
|
1564
|
-
choices:
|
|
1779
|
+
var GapSelectBankSchema = z8.looseObject({
|
|
1780
|
+
id: z8.string().min(1),
|
|
1781
|
+
choices: z8.array(GapSelectChoiceSchema).min(2)
|
|
1565
1782
|
});
|
|
1566
|
-
var GapSelectGapSchema =
|
|
1567
|
-
id:
|
|
1568
|
-
choices:
|
|
1569
|
-
bankId:
|
|
1570
|
-
correctChoiceId:
|
|
1571
|
-
feedback:
|
|
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()
|
|
1572
1789
|
});
|
|
1573
1790
|
function resolveChoices(gap, banks) {
|
|
1574
1791
|
if (gap.choices !== void 0) {
|
|
@@ -1587,24 +1804,25 @@ function placeholderCounts(passage) {
|
|
|
1587
1804
|
}
|
|
1588
1805
|
return counts;
|
|
1589
1806
|
}
|
|
1590
|
-
var GapSelectDataSchema =
|
|
1591
|
-
schemaVersion:
|
|
1592
|
-
type:
|
|
1593
|
-
id:
|
|
1594
|
-
title:
|
|
1595
|
-
passage:
|
|
1596
|
-
passageHtml:
|
|
1597
|
-
gaps:
|
|
1598
|
-
banks:
|
|
1599
|
-
scoringStrategy:
|
|
1600
|
-
presentation:
|
|
1601
|
-
shuffleChoices:
|
|
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(),
|
|
1602
1819
|
media: MediaSchema.optional(),
|
|
1603
1820
|
feedback: FeedbackSchema.optional(),
|
|
1604
|
-
passThreshold:
|
|
1605
|
-
locale:
|
|
1606
|
-
learningObjectives:
|
|
1607
|
-
difficultyLevel:
|
|
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()
|
|
1608
1826
|
}).refine(
|
|
1609
1827
|
(data) => {
|
|
1610
1828
|
const ids = (data.banks ?? []).map((bank) => bank.id);
|
|
@@ -1658,145 +1876,147 @@ var GapSelectDataSchema = z7.looseObject({
|
|
|
1658
1876
|
);
|
|
1659
1877
|
|
|
1660
1878
|
// src/schemas/written-response.ts
|
|
1661
|
-
import { z as
|
|
1662
|
-
var WrittenResponseRubricCriterionSchema =
|
|
1663
|
-
name:
|
|
1664
|
-
description:
|
|
1665
|
-
weight:
|
|
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)
|
|
1666
1884
|
});
|
|
1667
|
-
var WrittenResponseRubricSchema =
|
|
1668
|
-
label:
|
|
1669
|
-
criteria:
|
|
1885
|
+
var WrittenResponseRubricSchema = z9.looseObject({
|
|
1886
|
+
label: z9.string().optional(),
|
|
1887
|
+
criteria: z9.array(WrittenResponseRubricCriterionSchema).min(1)
|
|
1670
1888
|
});
|
|
1671
|
-
var RedactedWrittenResponseRubricCriterionSchema =
|
|
1672
|
-
name:
|
|
1673
|
-
description:
|
|
1674
|
-
weight:
|
|
1889
|
+
var RedactedWrittenResponseRubricCriterionSchema = z9.strictObject({
|
|
1890
|
+
name: z9.string().min(1),
|
|
1891
|
+
description: z9.string().optional(),
|
|
1892
|
+
weight: z9.number().min(0)
|
|
1675
1893
|
});
|
|
1676
|
-
var RedactedWrittenResponseRubricSchema =
|
|
1677
|
-
label:
|
|
1678
|
-
criteria:
|
|
1894
|
+
var RedactedWrittenResponseRubricSchema = z9.strictObject({
|
|
1895
|
+
label: z9.string().optional(),
|
|
1896
|
+
criteria: z9.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
|
|
1679
1897
|
});
|
|
1680
|
-
var WrittenResponseDataSchema =
|
|
1681
|
-
schemaVersion:
|
|
1682
|
-
type:
|
|
1683
|
-
id:
|
|
1684
|
-
title:
|
|
1685
|
-
prompt:
|
|
1686
|
-
promptHtml:
|
|
1687
|
-
minWords:
|
|
1688
|
-
maxWords:
|
|
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),
|
|
1689
1907
|
rubric: WrittenResponseRubricSchema.optional(),
|
|
1690
|
-
languageTarget:
|
|
1908
|
+
languageTarget: z9.string().optional(),
|
|
1691
1909
|
media: MediaSchema.optional(),
|
|
1692
1910
|
feedback: FeedbackSchema.optional(),
|
|
1693
|
-
passThreshold:
|
|
1694
|
-
locale:
|
|
1695
|
-
learningObjectives:
|
|
1696
|
-
difficultyLevel:
|
|
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()
|
|
1697
1916
|
}).refine((data) => data.maxWords >= data.minWords, {
|
|
1698
1917
|
error: "maxWords must be greater than or equal to minWords.",
|
|
1699
1918
|
path: ["maxWords"]
|
|
1700
1919
|
});
|
|
1701
1920
|
|
|
1702
1921
|
// src/schemas/redacted.ts
|
|
1703
|
-
import { z as
|
|
1922
|
+
import { z as z10 } from "zod/v4";
|
|
1704
1923
|
var redactedBase = {
|
|
1705
1924
|
/** Marker distinguishing a redacted projection from full activity data. */
|
|
1706
|
-
redacted:
|
|
1707
|
-
/** Slot identity, carried through redaction so the client and the plan agree. */
|
|
1708
|
-
slotKey:
|
|
1709
|
-
schemaVersion:
|
|
1710
|
-
id:
|
|
1711
|
-
title:
|
|
1712
|
-
media: RedactedMediaSchema.optional(),
|
|
1713
|
-
passThreshold:
|
|
1714
|
-
locale:
|
|
1715
|
-
learningObjectives:
|
|
1716
|
-
difficultyLevel:
|
|
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()
|
|
1717
1937
|
};
|
|
1718
|
-
var RedactedMultipleChoiceOptionMediaSchema =
|
|
1719
|
-
type:
|
|
1720
|
-
url:
|
|
1721
|
-
alt:
|
|
1722
|
-
captionsUrl:
|
|
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()
|
|
1723
1943
|
});
|
|
1724
|
-
var RedactedMultipleChoiceOptionSchema =
|
|
1725
|
-
id:
|
|
1726
|
-
text:
|
|
1944
|
+
var RedactedMultipleChoiceOptionSchema = z10.strictObject({
|
|
1945
|
+
id: z10.string().min(1),
|
|
1946
|
+
text: z10.string().min(1),
|
|
1727
1947
|
media: RedactedMultipleChoiceOptionMediaSchema.optional()
|
|
1728
1948
|
});
|
|
1729
|
-
var RedactedMultipleChoiceDataSchema =
|
|
1949
|
+
var RedactedMultipleChoiceDataSchema = z10.strictObject({
|
|
1730
1950
|
...redactedBase,
|
|
1731
|
-
type:
|
|
1732
|
-
question:
|
|
1733
|
-
questionHtml:
|
|
1734
|
-
mode:
|
|
1735
|
-
options:
|
|
1736
|
-
shuffle:
|
|
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()
|
|
1737
1957
|
});
|
|
1738
|
-
var RedactedBlankConfigSchema =
|
|
1739
|
-
id:
|
|
1740
|
-
hint:
|
|
1958
|
+
var RedactedBlankConfigSchema = z10.strictObject({
|
|
1959
|
+
id: z10.string().min(1),
|
|
1960
|
+
hint: z10.string().optional()
|
|
1741
1961
|
});
|
|
1742
|
-
var RedactedFillInTheBlanksDataSchema =
|
|
1962
|
+
var RedactedFillInTheBlanksDataSchema = z10.strictObject({
|
|
1743
1963
|
...redactedBase,
|
|
1744
|
-
type:
|
|
1745
|
-
passage:
|
|
1746
|
-
passageHtml:
|
|
1747
|
-
blanks:
|
|
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)
|
|
1748
1968
|
});
|
|
1749
|
-
var RedactedGapSelectChoiceSchema =
|
|
1750
|
-
id:
|
|
1751
|
-
text:
|
|
1969
|
+
var RedactedGapSelectChoiceSchema = z10.strictObject({
|
|
1970
|
+
id: z10.string().min(1),
|
|
1971
|
+
text: z10.string().min(1)
|
|
1752
1972
|
});
|
|
1753
|
-
var RedactedGapSelectBankSchema =
|
|
1754
|
-
id:
|
|
1755
|
-
choices:
|
|
1973
|
+
var RedactedGapSelectBankSchema = z10.strictObject({
|
|
1974
|
+
id: z10.string().min(1),
|
|
1975
|
+
choices: z10.array(RedactedGapSelectChoiceSchema).min(2)
|
|
1756
1976
|
});
|
|
1757
|
-
var RedactedGapSelectGapSchema =
|
|
1758
|
-
id:
|
|
1759
|
-
choices:
|
|
1760
|
-
bankId:
|
|
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()
|
|
1761
1981
|
});
|
|
1762
|
-
var RedactedGapSelectDataSchema =
|
|
1982
|
+
var RedactedGapSelectDataSchema = z10.strictObject({
|
|
1763
1983
|
...redactedBase,
|
|
1764
|
-
type:
|
|
1765
|
-
passage:
|
|
1766
|
-
passageHtml:
|
|
1767
|
-
gaps:
|
|
1768
|
-
banks:
|
|
1769
|
-
presentation:
|
|
1770
|
-
shuffleChoices:
|
|
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()
|
|
1771
1991
|
});
|
|
1772
|
-
var RedactedWrittenResponseDataSchema =
|
|
1992
|
+
var RedactedWrittenResponseDataSchema = z10.strictObject({
|
|
1773
1993
|
...redactedBase,
|
|
1774
|
-
type:
|
|
1775
|
-
prompt:
|
|
1776
|
-
promptHtml:
|
|
1777
|
-
minWords:
|
|
1778
|
-
maxWords:
|
|
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),
|
|
1779
1999
|
rubric: RedactedWrittenResponseRubricSchema.optional(),
|
|
1780
|
-
languageTarget:
|
|
2000
|
+
languageTarget: z10.string().optional()
|
|
1781
2001
|
});
|
|
1782
|
-
var RedactedDictationSlowMediaSchema =
|
|
1783
|
-
type:
|
|
2002
|
+
var RedactedDictationSlowMediaSchema = z10.strictObject({
|
|
2003
|
+
type: z10.literal("audio"),
|
|
1784
2004
|
url: MediaUrlSchema,
|
|
1785
|
-
alt:
|
|
2005
|
+
alt: z10.string().min(1).optional()
|
|
1786
2006
|
});
|
|
1787
|
-
var RedactedDictationDataSchema =
|
|
2007
|
+
var RedactedDictationDataSchema = z10.strictObject({
|
|
1788
2008
|
...redactedBase,
|
|
1789
|
-
type:
|
|
2009
|
+
type: z10.literal("dictation"),
|
|
1790
2010
|
slowMedia: RedactedDictationSlowMediaSchema.optional(),
|
|
1791
|
-
hints:
|
|
2011
|
+
hints: z10.strictObject({ mode: z10.literal("progressive-words") }).optional()
|
|
1792
2012
|
}).check((ctx) => {
|
|
1793
2013
|
const data = ctx.value;
|
|
1794
|
-
|
|
2014
|
+
for (const captions of captionFieldsOf(data.media)) {
|
|
1795
2015
|
ctx.issues.push({
|
|
1796
2016
|
code: "custom",
|
|
1797
|
-
input: data.media
|
|
2017
|
+
input: data.media?.[captions],
|
|
1798
2018
|
message: "A dictation recording cannot carry captions: the captions are the answer, so this payload is not learner-safe.",
|
|
1799
|
-
path: ["media",
|
|
2019
|
+
path: ["media", captions]
|
|
1800
2020
|
});
|
|
1801
2021
|
}
|
|
1802
2022
|
if (data.slowMedia !== void 0 && data.media?.playback?.maxPlays !== void 0) {
|
|
@@ -1816,23 +2036,23 @@ var RedactedDictationDataSchema = z9.strictObject({
|
|
|
1816
2036
|
});
|
|
1817
2037
|
}
|
|
1818
2038
|
});
|
|
1819
|
-
var RedactedReadAloudDataSchema =
|
|
2039
|
+
var RedactedReadAloudDataSchema = z10.strictObject({
|
|
1820
2040
|
...redactedBase,
|
|
1821
|
-
type:
|
|
1822
|
-
locale:
|
|
1823
|
-
instructions:
|
|
1824
|
-
referenceText:
|
|
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),
|
|
1825
2045
|
slowMedia: ReadAloudSlowMediaSchema.optional(),
|
|
1826
|
-
recording:
|
|
1827
|
-
maxSeconds:
|
|
1828
|
-
minSeconds:
|
|
1829
|
-
maxTakes:
|
|
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()
|
|
1830
2050
|
}),
|
|
1831
|
-
scoring:
|
|
1832
|
-
dimensions:
|
|
1833
|
-
|
|
1834
|
-
name:
|
|
1835
|
-
weight:
|
|
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)
|
|
1836
2056
|
})
|
|
1837
2057
|
).min(1).max(READ_ALOUD_DIMENSIONS.length)
|
|
1838
2058
|
})
|
|
@@ -1872,197 +2092,6 @@ var RedactedReadAloudDataSchema = z9.strictObject({
|
|
|
1872
2092
|
}
|
|
1873
2093
|
});
|
|
1874
2094
|
|
|
1875
|
-
// src/scoring/dictation/edit-distance.ts
|
|
1876
|
-
function codePoints(text) {
|
|
1877
|
-
return Array.from(text, (character) => character.codePointAt(0));
|
|
1878
|
-
}
|
|
1879
|
-
function editDistance(a, b) {
|
|
1880
|
-
if (a === b) {
|
|
1881
|
-
return 0;
|
|
1882
|
-
}
|
|
1883
|
-
let left = codePoints(a);
|
|
1884
|
-
let right = codePoints(b);
|
|
1885
|
-
if (left.length < right.length) {
|
|
1886
|
-
[left, right] = [right, left];
|
|
1887
|
-
}
|
|
1888
|
-
if (right.length === 0) {
|
|
1889
|
-
return left.length;
|
|
1890
|
-
}
|
|
1891
|
-
let previous = new Uint32Array(right.length + 1);
|
|
1892
|
-
let current = new Uint32Array(right.length + 1);
|
|
1893
|
-
for (let j = 0; j <= right.length; j += 1) {
|
|
1894
|
-
previous[j] = j;
|
|
1895
|
-
}
|
|
1896
|
-
for (let i = 1; i <= left.length; i += 1) {
|
|
1897
|
-
current[0] = i;
|
|
1898
|
-
const leftPoint = left[i - 1];
|
|
1899
|
-
for (let j = 1; j <= right.length; j += 1) {
|
|
1900
|
-
const deletion = previous[j] + 1;
|
|
1901
|
-
const insertion = current[j - 1] + 1;
|
|
1902
|
-
const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
|
|
1903
|
-
current[j] = Math.min(deletion, insertion, substitution);
|
|
1904
|
-
}
|
|
1905
|
-
[previous, current] = [current, previous];
|
|
1906
|
-
}
|
|
1907
|
-
return previous[right.length];
|
|
1908
|
-
}
|
|
1909
|
-
function similarity(a, b) {
|
|
1910
|
-
const max = Math.max(codePoints(a).length, codePoints(b).length);
|
|
1911
|
-
return max === 0 ? 0 : (max - editDistance(a, b)) / max;
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
// src/scoring/dictation/align.ts
|
|
1915
|
-
function dictationReferenceWords(data) {
|
|
1916
|
-
return rawCandidates(data).map(
|
|
1917
|
-
(candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
|
|
1918
|
-
itemId: `w${index + 1}`,
|
|
1919
|
-
word
|
|
1920
|
-
}))
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
function alignDictation(data, text) {
|
|
1924
|
-
const raw = typeof text === "string" ? text : "";
|
|
1925
|
-
const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
|
|
1926
|
-
const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
|
|
1927
|
-
const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
|
|
1928
|
-
const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
|
|
1929
|
-
const candidates = [];
|
|
1930
|
-
rawCandidates(data).forEach((candidate, index) => {
|
|
1931
|
-
const candidateText = normalizeCandidate(candidate, data);
|
|
1932
|
-
if (candidateText !== "") {
|
|
1933
|
-
candidates.push({ index, text: candidateText });
|
|
1934
|
-
}
|
|
1935
|
-
});
|
|
1936
|
-
if (candidates.length === 0) {
|
|
1937
|
-
return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
|
|
1938
|
-
}
|
|
1939
|
-
let chosen = candidates[0];
|
|
1940
|
-
let best = similarity(chosen.text, attempt);
|
|
1941
|
-
for (const candidate of candidates.slice(1)) {
|
|
1942
|
-
const candidateSimilarity = similarity(candidate.text, attempt);
|
|
1943
|
-
if (candidateSimilarity > best) {
|
|
1944
|
-
best = candidateSimilarity;
|
|
1945
|
-
chosen = candidate;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
return {
|
|
1949
|
-
candidateIndex: chosen.index,
|
|
1950
|
-
reference: chosen.text,
|
|
1951
|
-
attempt,
|
|
1952
|
-
truncated,
|
|
1953
|
-
similarity: best,
|
|
1954
|
-
words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
function rawCandidates(data) {
|
|
1958
|
-
return [
|
|
1959
|
-
data.transcript,
|
|
1960
|
-
...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
|
|
1961
|
-
];
|
|
1962
|
-
}
|
|
1963
|
-
function normalizeCandidate(candidate, data) {
|
|
1964
|
-
return typeof candidate === "string" ? cutAt(
|
|
1965
|
-
normalizeDictationText(
|
|
1966
|
-
truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
|
|
1967
|
-
data.tolerance
|
|
1968
|
-
),
|
|
1969
|
-
DICTATION_MAX_TRANSCRIPT_LENGTH
|
|
1970
|
-
) : "";
|
|
1971
|
-
}
|
|
1972
|
-
function cutAt(normalized, max) {
|
|
1973
|
-
const cut = truncateCodePoints(normalized, max);
|
|
1974
|
-
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
1975
|
-
}
|
|
1976
|
-
function tokensOf(normalized) {
|
|
1977
|
-
return normalized === "" ? [] : normalized.split(" ");
|
|
1978
|
-
}
|
|
1979
|
-
var STEP = {
|
|
1980
|
-
/** A reference element paired with an attempt element — equal or substituted. */
|
|
1981
|
-
pair: 0,
|
|
1982
|
-
/** A reference element the attempt lacks. */
|
|
1983
|
-
missing: 1,
|
|
1984
|
-
/** An attempt element the reference lacks. */
|
|
1985
|
-
extra: 2
|
|
1986
|
-
};
|
|
1987
|
-
function alignSequences(reference, attempt) {
|
|
1988
|
-
const rows = reference.length;
|
|
1989
|
-
const columns = attempt.length;
|
|
1990
|
-
const width = columns + 1;
|
|
1991
|
-
const steps = new Uint8Array((rows + 1) * width);
|
|
1992
|
-
let previous = new Uint32Array(width);
|
|
1993
|
-
let current = new Uint32Array(width);
|
|
1994
|
-
for (let j2 = 0; j2 <= columns; j2 += 1) {
|
|
1995
|
-
previous[j2] = j2;
|
|
1996
|
-
steps[j2] = STEP.extra;
|
|
1997
|
-
}
|
|
1998
|
-
for (let i2 = 1; i2 <= rows; i2 += 1) {
|
|
1999
|
-
current[0] = i2;
|
|
2000
|
-
steps[i2 * width] = STEP.missing;
|
|
2001
|
-
const referenceItem = reference[i2 - 1];
|
|
2002
|
-
for (let j2 = 1; j2 <= columns; j2 += 1) {
|
|
2003
|
-
const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
|
|
2004
|
-
const missing = previous[j2] + 1;
|
|
2005
|
-
const extra = current[j2 - 1] + 1;
|
|
2006
|
-
const cost = Math.min(paired, missing, extra);
|
|
2007
|
-
current[j2] = cost;
|
|
2008
|
-
steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
|
|
2009
|
-
}
|
|
2010
|
-
[previous, current] = [current, previous];
|
|
2011
|
-
}
|
|
2012
|
-
const path = [];
|
|
2013
|
-
let i = rows;
|
|
2014
|
-
let j = columns;
|
|
2015
|
-
while (i > 0 || j > 0) {
|
|
2016
|
-
const step = steps[i * width + j];
|
|
2017
|
-
if (step === STEP.pair) {
|
|
2018
|
-
i -= 1;
|
|
2019
|
-
j -= 1;
|
|
2020
|
-
} else if (step === STEP.missing) {
|
|
2021
|
-
i -= 1;
|
|
2022
|
-
} else {
|
|
2023
|
-
j -= 1;
|
|
2024
|
-
}
|
|
2025
|
-
path.push({ step, referenceIndex: i, attemptIndex: j });
|
|
2026
|
-
}
|
|
2027
|
-
return path.reverse();
|
|
2028
|
-
}
|
|
2029
|
-
function alignWords(reference, attempt) {
|
|
2030
|
-
const words = [];
|
|
2031
|
-
let ordinal = 0;
|
|
2032
|
-
for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
|
|
2033
|
-
if (step === STEP.extra) {
|
|
2034
|
-
words.push({
|
|
2035
|
-
reference: "",
|
|
2036
|
-
attempt: attempt[attemptIndex],
|
|
2037
|
-
similarity: 0,
|
|
2038
|
-
status: "extra"
|
|
2039
|
-
});
|
|
2040
|
-
continue;
|
|
2041
|
-
}
|
|
2042
|
-
ordinal += 1;
|
|
2043
|
-
const referenceWord = reference[referenceIndex];
|
|
2044
|
-
if (step === STEP.missing) {
|
|
2045
|
-
words.push({
|
|
2046
|
-
itemId: `w${ordinal}`,
|
|
2047
|
-
reference: referenceWord,
|
|
2048
|
-
attempt: "",
|
|
2049
|
-
similarity: 0,
|
|
2050
|
-
status: "missing"
|
|
2051
|
-
});
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
const attemptWord = attempt[attemptIndex];
|
|
2055
|
-
words.push({
|
|
2056
|
-
itemId: `w${ordinal}`,
|
|
2057
|
-
reference: referenceWord,
|
|
2058
|
-
attempt: attemptWord,
|
|
2059
|
-
similarity: similarity(referenceWord, attemptWord),
|
|
2060
|
-
status: referenceWord === attemptWord ? "correct" : "incorrect"
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
return words;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
2095
|
// src/scoring/text-match.ts
|
|
2067
2096
|
var COMBINING_MARKS_RE = /\p{M}/gu;
|
|
2068
2097
|
var PUNCTUATION_RE2 = /\p{P}/gu;
|
|
@@ -2187,6 +2216,7 @@ var DRAFT_ISSUE_SEVERITY = {
|
|
|
2187
2216
|
scoring_strategy_required: "incomplete",
|
|
2188
2217
|
pass_threshold_invalid: "invalid",
|
|
2189
2218
|
difficulty_level_invalid: "invalid",
|
|
2219
|
+
ai_permissions_invalid: "invalid",
|
|
2190
2220
|
feedback_empty: "incomplete",
|
|
2191
2221
|
redacted_data: "invalid",
|
|
2192
2222
|
media_type_required: "incomplete",
|
|
@@ -2421,6 +2451,30 @@ function checkSharedOptional(draft) {
|
|
|
2421
2451
|
)
|
|
2422
2452
|
);
|
|
2423
2453
|
}
|
|
2454
|
+
const ai = draft.ai;
|
|
2455
|
+
if (!isUnset(ai)) {
|
|
2456
|
+
if (!isRecord(ai)) {
|
|
2457
|
+
issues.push(
|
|
2458
|
+
issue(
|
|
2459
|
+
"ai_permissions_invalid",
|
|
2460
|
+
["ai"],
|
|
2461
|
+
"AI permissions must be an object of on/off flags, such as { hints: false }."
|
|
2462
|
+
)
|
|
2463
|
+
);
|
|
2464
|
+
} else {
|
|
2465
|
+
for (const key of ["explanations", "hints"]) {
|
|
2466
|
+
if (!isUnset(ai[key]) && typeof ai[key] !== "boolean") {
|
|
2467
|
+
issues.push(
|
|
2468
|
+
issue(
|
|
2469
|
+
"ai_permissions_invalid",
|
|
2470
|
+
["ai", key],
|
|
2471
|
+
"Set this to true or false, or remove it."
|
|
2472
|
+
)
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2424
2478
|
const feedback = draft.feedback;
|
|
2425
2479
|
if (isRecord(feedback)) {
|
|
2426
2480
|
for (const key of ["correct", "incorrect"]) {
|
|
@@ -2656,10 +2710,8 @@ function checkRecording(media) {
|
|
|
2656
2710
|
)
|
|
2657
2711
|
);
|
|
2658
2712
|
}
|
|
2659
|
-
|
|
2660
|
-
issues.push(
|
|
2661
|
-
issue("dc_captions_not_allowed", ["media", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2662
|
-
);
|
|
2713
|
+
for (const captions of captionFieldsOf(media, (captionsUrl) => !isUnset(captionsUrl))) {
|
|
2714
|
+
issues.push(issue("dc_captions_not_allowed", ["media", captions], CAPTIONS_ARE_THE_ANSWER));
|
|
2663
2715
|
}
|
|
2664
2716
|
return issues;
|
|
2665
2717
|
}
|
|
@@ -2684,10 +2736,8 @@ function checkSlowRecording(draft) {
|
|
|
2684
2736
|
DictationSlowMediaSchema
|
|
2685
2737
|
)
|
|
2686
2738
|
);
|
|
2687
|
-
|
|
2688
|
-
issues.push(
|
|
2689
|
-
issue("dc_captions_not_allowed", ["slowMedia", "captionsUrl"], CAPTIONS_ARE_THE_ANSWER)
|
|
2690
|
-
);
|
|
2739
|
+
for (const captions of captionFieldsOf(slow, (captionsUrl) => !isUnset(captionsUrl))) {
|
|
2740
|
+
issues.push(issue("dc_captions_not_allowed", ["slowMedia", captions], CAPTIONS_ARE_THE_ANSWER));
|
|
2691
2741
|
}
|
|
2692
2742
|
if (slow.playback !== void 0) {
|
|
2693
2743
|
issues.push(
|
|
@@ -4064,7 +4114,11 @@ var SHARED_PUBLIC_FIELDS = {
|
|
|
4064
4114
|
passThreshold: "public",
|
|
4065
4115
|
locale: "public",
|
|
4066
4116
|
learningObjectives: "public",
|
|
4067
|
-
difficultyLevel: "public"
|
|
4117
|
+
difficultyLevel: "public",
|
|
4118
|
+
// What an author allows AI to do for the item. Public, field by field: a
|
|
4119
|
+
// review that explains an answer must still honour an author who said no,
|
|
4120
|
+
// and an unclassified key parked inside it must not ride along.
|
|
4121
|
+
ai: { explanations: "public", hints: "public" }
|
|
4068
4122
|
};
|
|
4069
4123
|
var FEEDBACK_FIELD_POLICY = {
|
|
4070
4124
|
correct: "answer-key",
|
|
@@ -4337,10 +4391,18 @@ registerActivityType(dictationType);
|
|
|
4337
4391
|
registerActivityType(readAloudType);
|
|
4338
4392
|
|
|
4339
4393
|
export {
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4394
|
+
AiPermissionsSchema,
|
|
4395
|
+
FeedbackSchema,
|
|
4396
|
+
MediaUrlSchema,
|
|
4397
|
+
NativeControlHintSchema,
|
|
4398
|
+
MediaPlaybackSchema,
|
|
4399
|
+
MediaTrackSchema,
|
|
4400
|
+
MediaSchema,
|
|
4401
|
+
RedactedMediaSchema,
|
|
4402
|
+
PLACEHOLDER_RE,
|
|
4403
|
+
TextMatchPolicySchema,
|
|
4404
|
+
BlankConfigSchema,
|
|
4405
|
+
FillInTheBlanksDataSchema,
|
|
4344
4406
|
withValidationScope,
|
|
4345
4407
|
DICTATION_MAX_TRANSCRIPT_LENGTH,
|
|
4346
4408
|
DICTATION_MAX_TEXT_LENGTH,
|
|
@@ -4349,18 +4411,19 @@ export {
|
|
|
4349
4411
|
DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
|
|
4350
4412
|
codePointLength,
|
|
4351
4413
|
normalizeDictationText,
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4414
|
+
dictationReferenceWords,
|
|
4415
|
+
alignDictation,
|
|
4416
|
+
STEP,
|
|
4417
|
+
alignSequences,
|
|
4418
|
+
ActivitySchemaError,
|
|
4419
|
+
UnknownActivityTypeError,
|
|
4420
|
+
RedactedScoringError,
|
|
4421
|
+
DeferredScoringError,
|
|
4359
4422
|
DictationSlowMediaSchema,
|
|
4360
4423
|
DictationEquivalenceSchema,
|
|
4361
4424
|
DictationToleranceSchema,
|
|
4362
4425
|
DictationDataSchema,
|
|
4363
|
-
|
|
4426
|
+
groupCaptionsField,
|
|
4364
4427
|
GROUP_CAPTIONS_REVEAL_DICTATION,
|
|
4365
4428
|
DRAFT_ISSUE_SEVERITY,
|
|
4366
4429
|
issue,
|
|
@@ -4373,9 +4436,6 @@ export {
|
|
|
4373
4436
|
covers,
|
|
4374
4437
|
pathKey,
|
|
4375
4438
|
coveredPathsOf,
|
|
4376
|
-
TextMatchPolicySchema,
|
|
4377
|
-
BlankConfigSchema,
|
|
4378
|
-
FillInTheBlanksDataSchema,
|
|
4379
4439
|
MultipleChoiceOptionMediaSchema,
|
|
4380
4440
|
MultipleChoiceOptionSchema,
|
|
4381
4441
|
MultipleChoiceDataSchema,
|
|
@@ -4409,10 +4469,6 @@ export {
|
|
|
4409
4469
|
RedactedDictationSlowMediaSchema,
|
|
4410
4470
|
RedactedDictationDataSchema,
|
|
4411
4471
|
RedactedReadAloudDataSchema,
|
|
4412
|
-
dictationReferenceWords,
|
|
4413
|
-
alignDictation,
|
|
4414
|
-
STEP,
|
|
4415
|
-
alignSequences,
|
|
4416
4472
|
levenshteinDistance,
|
|
4417
4473
|
matchText,
|
|
4418
4474
|
defineActivityType,
|
|
@@ -4427,4 +4483,4 @@ export {
|
|
|
4427
4483
|
dictationType,
|
|
4428
4484
|
readAloudType
|
|
4429
4485
|
};
|
|
4430
|
-
//# sourceMappingURL=chunk-
|
|
4486
|
+
//# sourceMappingURL=chunk-D3GP5MET.js.map
|