@intellectif/lk-core 0.16.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/{activity-DcxtpX7Z.d.cts → activity-BmjBjXVJ.d.cts} +213 -2
  3. package/dist/{activity-DcxtpX7Z.d.ts → activity-BmjBjXVJ.d.ts} +213 -2
  4. package/dist/{chunk-FJJ2GVBA.cjs → chunk-2CV6NU32.cjs} +19 -19
  5. package/dist/{chunk-FJJ2GVBA.cjs.map → chunk-2CV6NU32.cjs.map} +1 -1
  6. package/dist/{chunk-IRUD73IQ.js → chunk-D3GP5MET.js} +683 -638
  7. package/dist/chunk-D3GP5MET.js.map +1 -0
  8. package/dist/{chunk-F7UOLKWV.cjs → chunk-D4TOPSHV.cjs} +518 -473
  9. package/dist/chunk-D4TOPSHV.cjs.map +1 -0
  10. package/dist/{chunk-VFTVZJO5.cjs → chunk-EOFWSGLU.cjs} +4 -4
  11. package/dist/{chunk-VFTVZJO5.cjs.map → chunk-EOFWSGLU.cjs.map} +1 -1
  12. package/dist/{chunk-S63IOKFP.js → chunk-WXS5EB65.js} +2 -2
  13. package/dist/{chunk-OXF7VT7S.js → chunk-X5DAXBJI.js} +2 -2
  14. package/dist/{chunk-6P6XK2LI.cjs → chunk-XNMKDFPW.cjs} +31 -31
  15. package/dist/{chunk-6P6XK2LI.cjs.map → chunk-XNMKDFPW.cjs.map} +1 -1
  16. package/dist/{chunk-GOZGBVJP.js → chunk-Z7QQZ53L.js} +2 -2
  17. package/dist/{chunk-GOZGBVJP.js.map → chunk-Z7QQZ53L.js.map} +1 -1
  18. package/dist/{index-BWpwZRQw.d.cts → index-CvfeD_cU.d.cts} +144 -86
  19. package/dist/{index-DRJqwO2y.d.ts → index-G_KusUcK.d.ts} +144 -86
  20. package/dist/index.cjs +542 -127
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +122 -5
  23. package/dist/index.d.ts +122 -5
  24. package/dist/index.js +422 -7
  25. package/dist/index.js.map +1 -1
  26. package/dist/schemas.cjs +5 -3
  27. package/dist/schemas.cjs.map +1 -1
  28. package/dist/schemas.d.cts +2 -2
  29. package/dist/schemas.d.ts +2 -2
  30. package/dist/schemas.js +4 -2
  31. package/dist/scoring.cjs +3 -3
  32. package/dist/scoring.d.cts +2 -2
  33. package/dist/scoring.d.ts +2 -2
  34. package/dist/scoring.js +2 -2
  35. package/dist/xapi.cjs +3 -3
  36. package/dist/xapi.d.cts +1 -1
  37. package/dist/xapi.d.ts +1 -1
  38. package/dist/xapi.js +2 -2
  39. package/package.json +1 -1
  40. package/dist/chunk-F7UOLKWV.cjs.map +0 -1
  41. package/dist/chunk-IRUD73IQ.js.map +0 -1
  42. /package/dist/{chunk-S63IOKFP.js.map → chunk-WXS5EB65.js.map} +0 -0
  43. /package/dist/{chunk-OXF7VT7S.js.map → chunk-X5DAXBJI.js.map} +0 -0
@@ -1,42 +1,205 @@
1
- // src/errors.ts
2
- var ActivitySchemaError = class extends Error {
3
- constructor(activityType, errors) {
4
- super(`Invalid activity data for type "${activityType}"`);
5
- this.activityType = activityType;
6
- this.errors = errors;
7
- this.name = "ActivitySchemaError";
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
- activityType;
10
- errors;
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 UnknownActivityTypeError = class extends Error {
13
- constructor(activityType) {
14
- super(`Activity type "${activityType}" is not registered`);
15
- this.activityType = activityType;
16
- this.name = "UnknownActivityTypeError";
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
- activityType;
19
- };
20
- var RedactedScoringError = class extends Error {
21
- constructor(activityType) {
22
- super(
23
- `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.`
24
- );
25
- this.activityType = activityType;
26
- this.name = "RedactedScoringError";
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
- activityType;
29
- };
30
- var DeferredScoringError = class extends Error {
31
- constructor(activityType) {
32
- super(
33
- `Activity type "${activityType}" is graded asynchronously and has no synchronous score. Use evaluate() \u2014 it returns { status: 'deferred' } for this type.`
34
- );
35
- this.activityType = activityType;
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
- activityType;
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/schemas/feedback.ts
916
- import { z } from "zod/v4";
917
- var FeedbackSchema = z.looseObject({
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 tracksAreDistinct(m) {
973
- if (m.tracks === void 0) {
974
- return true;
1082
+ function editDistance(a, b) {
1083
+ if (a === b) {
1084
+ return 0;
975
1085
  }
976
- const defaults = m.tracks.filter((track) => track.default === true).length;
977
- const keys = new Set(m.tracks.map((track) => `${track.kind}:${track.srclang.toLowerCase()}`));
978
- return defaults <= 1 && keys.size === m.tracks.length;
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
- ).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
1001
- 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.",
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
- ).refine((m) => m.playback?.maxPlays === void 0 || m.playback.seek !== "allow", {
1013
- 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.',
1014
- path: ["playback", "seek"]
1015
- }).refine(
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
- ).refine(
1022
- (m) => m.playback?.nativeControlHints === void 0 || m.playback.controls !== "minimal" && m.playback.maxPlays === void 0 && m.playback.seek !== "none" && m.playback.rate !== "fixed",
1023
- {
1024
- 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.",
1025
- path: ["playback", "nativeControlHints"]
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
- var RedactedMediaSchema = z2.strictObject({
1029
- type: z2.enum(["image", "audio", "video", "embed"]),
1030
- url: MediaUrlSchema,
1031
- alt: z2.string().min(1).optional(),
1032
- captionsUrl: MediaUrlSchema.optional(),
1033
- tracks: z2.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
1034
- poster: MediaUrlSchema.optional(),
1035
- playback: MediaPlaybackSchema.optional()
1036
- }).refine(
1037
- (m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
1038
- {
1039
- error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
1040
- path: ["alt"]
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
- ).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
1043
- 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.",
1044
- path: ["url"]
1045
- }).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
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 z3 } from "zod/v4";
1049
- var DictationSlowMediaSchema = z3.looseObject({
1050
- type: z3.literal("audio"),
1310
+ import { z as z5 } from "zod/v4";
1311
+ var DictationSlowMediaSchema = z5.looseObject({
1312
+ type: z5.literal("audio"),
1051
1313
  url: MediaUrlSchema,
1052
- alt: z3.string().min(1).optional()
1314
+ alt: z5.string().min(1).optional()
1053
1315
  });
1054
- var RuleTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_EQUIVALENCE_LENGTH });
1055
- var TranscriptTextSchema = z3.string().min(1).meta({ maxLength: DICTATION_MAX_TRANSCRIPT_LENGTH });
1056
- var DictationEquivalenceSchema = z3.looseObject({
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 = z3.looseObject({
1067
- equivalences: z3.array(DictationEquivalenceSchema).max(DICTATION_MAX_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 z3.core.$ZodCheck({ check: "custom", when: () => true });
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 = z3.looseObject({
1095
- schemaVersion: z3.literal("1.0"),
1096
- type: z3.literal("dictation"),
1097
- id: z3.string().min(1),
1098
- title: z3.string().min(1),
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: z3.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1362
+ acceptedTranscripts: z5.array(TranscriptTextSchema).max(DICTATION_MAX_ACCEPTED_TRANSCRIPTS).optional(),
1101
1363
  media: MediaSchema.optional(),
1102
1364
  slowMedia: DictationSlowMediaSchema.optional(),
1103
- hints: z3.looseObject({ mode: z3.literal("progressive-words") }).optional(),
1365
+ hints: z5.looseObject({ mode: z5.literal("progressive-words") }).optional(),
1104
1366
  tolerance: DictationToleranceSchema.optional(),
1105
1367
  feedback: FeedbackSchema.optional(),
1106
- passThreshold: z3.number().min(0).max(1).optional(),
1107
- locale: z3.string().optional(),
1108
- learningObjectives: z3.array(z3.string()).optional(),
1109
- difficultyLevel: z3.literal([1, 2, 3, 4, 5]).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()
1110
1373
  });
1111
1374
  var DictationDataSchema = DictationDataShape.check(
1112
1375
  checkEvenAfterIssues((ctx) => {
@@ -1276,78 +1539,15 @@ function groupCaptionsField(group, written) {
1276
1539
  }
1277
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.";
1278
1541
 
1279
- // src/schemas/fill-in-the-blanks.ts
1280
- import { z as z4 } from "zod/v4";
1281
- var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
1282
- var TextMatchPolicySchema = z4.looseObject({
1283
- caseSensitive: z4.boolean().optional(),
1284
- trim: z4.boolean().optional(),
1285
- normalize: z4.enum(["none", "NFC", "NFKC"]).optional(),
1286
- foldDiacritics: z4.boolean().optional(),
1287
- collapseInnerWhitespace: z4.boolean().optional(),
1288
- ignorePunctuation: z4.boolean().optional(),
1289
- levenshtein: z4.number().int().min(0).optional(),
1290
- locale: z4.string().optional()
1291
- });
1292
- var BlankConfigSchema = z4.looseObject({
1293
- id: z4.string().min(1),
1294
- acceptedAnswers: z4.array(
1295
- z4.string().min(1).refine((answer) => answer.trim().length > 0, {
1296
- error: "Accepted answers must contain non-whitespace characters."
1297
- })
1298
- ).min(1),
1299
- caseSensitive: z4.boolean().optional(),
1300
- trimWhitespace: z4.boolean().optional(),
1301
- match: TextMatchPolicySchema.optional(),
1302
- hint: z4.string().optional(),
1303
- feedback: z4.string().optional()
1304
- });
1305
- var FillInTheBlanksDataSchema = z4.looseObject({
1306
- schemaVersion: z4.literal("1.0"),
1307
- type: z4.literal("fill-in-the-blanks"),
1308
- id: z4.string().min(1),
1309
- title: z4.string().min(1),
1310
- passage: z4.string().min(1),
1311
- passageHtml: z4.string().optional(),
1312
- blanks: z4.array(BlankConfigSchema).min(1),
1313
- scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
1314
- media: MediaSchema.optional(),
1315
- feedback: FeedbackSchema.optional(),
1316
- passThreshold: z4.number().min(0).max(1).optional(),
1317
- locale: z4.string().optional(),
1318
- learningObjectives: z4.array(z4.string()).optional(),
1319
- difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
1320
- }).refine(
1321
- (data) => {
1322
- const placeholderCounts2 = /* @__PURE__ */ new Map();
1323
- for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
1324
- const id = match[1];
1325
- placeholderCounts2.set(id, (placeholderCounts2.get(id) ?? 0) + 1);
1326
- }
1327
- const blankIds = data.blanks.map((blank) => blank.id);
1328
- if (new Set(blankIds).size !== blankIds.length) {
1329
- return false;
1330
- }
1331
- if (placeholderCounts2.size !== blankIds.length) {
1332
- return false;
1333
- }
1334
- return blankIds.every((id) => placeholderCounts2.get(id) === 1);
1335
- },
1336
- {
1337
- error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
1338
- path: ["passage"]
1339
- }
1340
- );
1341
-
1342
1542
  // src/schemas/multiple-choice.ts
1343
- import { z as z5 } from "zod/v4";
1344
- var MultipleChoiceOptionMediaSchema = z5.looseObject({
1543
+ import { z as z6 } from "zod/v4";
1544
+ var MultipleChoiceOptionMediaSchema = z6.looseObject({
1345
1545
  // No `embed`: an iframe swallows the click that selects the option, so the
1346
1546
  // learner could not choose it. No `video`: a native control bar inside the
1347
1547
  // option's label eats the same click.
1348
- type: z5.enum(["image", "audio"]),
1548
+ type: z6.enum(["image", "audio"]),
1349
1549
  url: MediaUrlSchema,
1350
- alt: z5.string().min(1).optional(),
1550
+ alt: z6.string().min(1).optional(),
1351
1551
  captionsUrl: MediaUrlSchema.optional()
1352
1552
  }).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
1353
1553
  error: "An image option requires a non-empty `alt`.",
@@ -1356,30 +1556,31 @@ var MultipleChoiceOptionMediaSchema = z5.looseObject({
1356
1556
  error: "An option carries no playback policy. maxPlays, seek and rate are enforced only on the media above the question.",
1357
1557
  path: ["playback"]
1358
1558
  });
1359
- var MultipleChoiceOptionSchema = z5.looseObject({
1360
- id: z5.string().min(1),
1361
- text: z5.string().min(1),
1362
- isCorrect: z5.boolean(),
1363
- feedback: z5.string().optional(),
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(),
1364
1564
  media: MultipleChoiceOptionMediaSchema.optional()
1365
1565
  });
1366
- var MultipleChoiceDataSchema = z5.looseObject({
1367
- schemaVersion: z5.literal("1.0"),
1368
- type: z5.literal("multiple-choice"),
1369
- id: z5.string().min(1),
1370
- title: z5.string().min(1),
1371
- question: z5.string().min(1),
1372
- questionHtml: z5.string().optional(),
1373
- mode: z5.enum(["single", "multi"]),
1374
- options: z5.array(MultipleChoiceOptionSchema).min(2).max(26),
1375
- scoringStrategy: z5.enum(["all-or-nothing", "partial"]),
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"]),
1376
1576
  media: MediaSchema.optional(),
1377
1577
  feedback: FeedbackSchema.optional(),
1378
- passThreshold: z5.number().min(0).max(1).optional(),
1379
- shuffle: z5.boolean().optional(),
1380
- locale: z5.string().optional(),
1381
- learningObjectives: z5.array(z5.string()).optional(),
1382
- difficultyLevel: z5.literal([1, 2, 3, 4, 5]).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()
1383
1584
  }).refine((data) => data.options.some((option) => option.isCorrect), {
1384
1585
  error: "At least one option must be marked correct.",
1385
1586
  path: ["options"]
@@ -1403,7 +1604,7 @@ var SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
1403
1604
  var SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
1404
1605
 
1405
1606
  // src/schemas/read-aloud.ts
1406
- import { z as z6 } from "zod/v4";
1607
+ import { z as z7 } from "zod/v4";
1407
1608
 
1408
1609
  // src/scoring/speech/locale.ts
1409
1610
  var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
@@ -1411,49 +1612,50 @@ var CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$
1411
1612
  // src/schemas/read-aloud.ts
1412
1613
  var READ_ALOUD_DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
1413
1614
  var READ_ALOUD_MAX_DIMENSIONS = READ_ALOUD_DIMENSIONS.length;
1414
- var ReadAloudSlowMediaSchema = z6.strictObject({
1415
- type: z6.literal("audio"),
1615
+ var ReadAloudSlowMediaSchema = z7.strictObject({
1616
+ type: z7.literal("audio"),
1416
1617
  url: MediaUrlSchema,
1417
- alt: z6.string().min(1).optional()
1618
+ alt: z7.string().min(1).optional()
1418
1619
  });
1419
- var ReferenceTextSchema = z6.string().min(1).refine((text) => text.trim() !== "", {
1620
+ var ReferenceTextSchema = z7.string().min(1).refine((text) => text.trim() !== "", {
1420
1621
  error: "The text to read aloud must contain something to read."
1421
1622
  }).meta({ maxLength: READ_ALOUD_MAX_REFERENCE_LENGTH });
1422
- var RecordingBoundsSchema = z6.looseObject({
1423
- maxSeconds: z6.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
1424
- minSeconds: z6.number().min(0).optional(),
1425
- maxTakes: z6.number().int().min(1).max(READ_ALOUD_MAX_TAKES).optional()
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()
1426
1627
  });
1427
- var ReadAloudDimensionWeightSchema = z6.looseObject({
1428
- name: z6.enum(READ_ALOUD_DIMENSIONS),
1429
- weight: z6.number().min(0).max(READ_ALOUD_MAX_DIMENSION_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)
1430
1631
  });
1431
- var ReadAloudScoringSchema = z6.looseObject({
1432
- dimensions: z6.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
1632
+ var ReadAloudScoringSchema = z7.looseObject({
1633
+ dimensions: z7.array(ReadAloudDimensionWeightSchema).min(1).max(READ_ALOUD_MAX_DIMENSIONS)
1433
1634
  });
1434
1635
  function checkEvenAfterIssues2(check) {
1435
- const guard = new z6.core.$ZodCheck({ check: "custom", when: () => true });
1636
+ const guard = new z7.core.$ZodCheck({ check: "custom", when: () => true });
1436
1637
  guard._zod.check = check;
1437
1638
  return guard;
1438
1639
  }
1439
- var ReadAloudDataShape = z6.looseObject({
1440
- schemaVersion: z6.literal("1.0"),
1441
- type: z6.literal("read-aloud"),
1442
- id: z6.string().min(1),
1443
- title: z6.string().min(1),
1444
- instructions: z6.string().optional(),
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(),
1445
1646
  referenceText: ReferenceTextSchema,
1446
- locale: z6.string().regex(CANONICAL_LOCALE_RE, {
1647
+ locale: z7.string().regex(CANONICAL_LOCALE_RE, {
1447
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.`
1448
1649
  }),
1449
1650
  media: MediaSchema.optional(),
1450
1651
  slowMedia: ReadAloudSlowMediaSchema.optional(),
1451
1652
  recording: RecordingBoundsSchema,
1452
1653
  scoring: ReadAloudScoringSchema,
1453
- passThreshold: z6.number().min(0).max(1).optional(),
1654
+ passThreshold: z7.number().min(0).max(1).optional(),
1454
1655
  feedback: FeedbackSchema.optional(),
1455
- learningObjectives: z6.array(z6.string()).optional(),
1456
- difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional()
1656
+ learningObjectives: z7.array(z7.string()).optional(),
1657
+ difficultyLevel: z7.literal([1, 2, 3, 4, 5]).optional(),
1658
+ ai: AiPermissionsSchema.optional()
1457
1659
  });
1458
1660
  var ReadAloudDataSchema = ReadAloudDataShape.check(
1459
1661
  checkEvenAfterIssues2((ctx) => {
@@ -1569,21 +1771,21 @@ function countWords(text) {
1569
1771
  }
1570
1772
 
1571
1773
  // src/schemas/gap-select.ts
1572
- import { z as z7 } from "zod/v4";
1573
- var GapSelectChoiceSchema = z7.looseObject({
1574
- id: z7.string().min(1),
1575
- text: z7.string().min(1)
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)
1576
1778
  });
1577
- var GapSelectBankSchema = z7.looseObject({
1578
- id: z7.string().min(1),
1579
- choices: z7.array(GapSelectChoiceSchema).min(2)
1779
+ var GapSelectBankSchema = z8.looseObject({
1780
+ id: z8.string().min(1),
1781
+ choices: z8.array(GapSelectChoiceSchema).min(2)
1580
1782
  });
1581
- var GapSelectGapSchema = z7.looseObject({
1582
- id: z7.string().min(1),
1583
- choices: z7.array(GapSelectChoiceSchema).min(2).optional(),
1584
- bankId: z7.string().min(1).optional(),
1585
- correctChoiceId: z7.string().min(1),
1586
- feedback: z7.string().optional()
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()
1587
1789
  });
1588
1790
  function resolveChoices(gap, banks) {
1589
1791
  if (gap.choices !== void 0) {
@@ -1602,24 +1804,25 @@ function placeholderCounts(passage) {
1602
1804
  }
1603
1805
  return counts;
1604
1806
  }
1605
- var GapSelectDataSchema = z7.looseObject({
1606
- schemaVersion: z7.literal("1.0"),
1607
- type: z7.literal("gap-select"),
1608
- id: z7.string().min(1),
1609
- title: z7.string().min(1),
1610
- passage: z7.string().min(1),
1611
- passageHtml: z7.string().optional(),
1612
- gaps: z7.array(GapSelectGapSchema).min(1),
1613
- banks: z7.array(GapSelectBankSchema).optional(),
1614
- scoringStrategy: z7.enum(["all-or-nothing", "partial"]),
1615
- presentation: z7.literal("dropdown").optional(),
1616
- shuffleChoices: z7.boolean().optional(),
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(),
1617
1819
  media: MediaSchema.optional(),
1618
1820
  feedback: FeedbackSchema.optional(),
1619
- passThreshold: z7.number().min(0).max(1).optional(),
1620
- locale: z7.string().optional(),
1621
- learningObjectives: z7.array(z7.string()).optional(),
1622
- difficultyLevel: z7.literal([1, 2, 3, 4, 5]).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()
1623
1826
  }).refine(
1624
1827
  (data) => {
1625
1828
  const ids = (data.banks ?? []).map((bank) => bank.id);
@@ -1673,137 +1876,139 @@ var GapSelectDataSchema = z7.looseObject({
1673
1876
  );
1674
1877
 
1675
1878
  // src/schemas/written-response.ts
1676
- import { z as z8 } from "zod/v4";
1677
- var WrittenResponseRubricCriterionSchema = z8.looseObject({
1678
- name: z8.string().min(1),
1679
- description: z8.string().optional(),
1680
- weight: z8.number().min(0)
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)
1681
1884
  });
1682
- var WrittenResponseRubricSchema = z8.looseObject({
1683
- label: z8.string().optional(),
1684
- criteria: z8.array(WrittenResponseRubricCriterionSchema).min(1)
1885
+ var WrittenResponseRubricSchema = z9.looseObject({
1886
+ label: z9.string().optional(),
1887
+ criteria: z9.array(WrittenResponseRubricCriterionSchema).min(1)
1685
1888
  });
1686
- var RedactedWrittenResponseRubricCriterionSchema = z8.strictObject({
1687
- name: z8.string().min(1),
1688
- description: z8.string().optional(),
1689
- weight: z8.number().min(0)
1889
+ var RedactedWrittenResponseRubricCriterionSchema = z9.strictObject({
1890
+ name: z9.string().min(1),
1891
+ description: z9.string().optional(),
1892
+ weight: z9.number().min(0)
1690
1893
  });
1691
- var RedactedWrittenResponseRubricSchema = z8.strictObject({
1692
- label: z8.string().optional(),
1693
- criteria: z8.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
1894
+ var RedactedWrittenResponseRubricSchema = z9.strictObject({
1895
+ label: z9.string().optional(),
1896
+ criteria: z9.array(RedactedWrittenResponseRubricCriterionSchema).min(1)
1694
1897
  });
1695
- var WrittenResponseDataSchema = z8.looseObject({
1696
- schemaVersion: z8.literal("1.0"),
1697
- type: z8.literal("written-response"),
1698
- id: z8.string().min(1),
1699
- title: z8.string().min(1),
1700
- prompt: z8.string(),
1701
- promptHtml: z8.string().optional(),
1702
- minWords: z8.number().int().min(0),
1703
- maxWords: z8.number().int().min(1),
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),
1704
1907
  rubric: WrittenResponseRubricSchema.optional(),
1705
- languageTarget: z8.string().optional(),
1908
+ languageTarget: z9.string().optional(),
1706
1909
  media: MediaSchema.optional(),
1707
1910
  feedback: FeedbackSchema.optional(),
1708
- passThreshold: z8.number().min(0).max(1).optional(),
1709
- locale: z8.string().optional(),
1710
- learningObjectives: z8.array(z8.string()).optional(),
1711
- difficultyLevel: z8.literal([1, 2, 3, 4, 5]).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()
1712
1916
  }).refine((data) => data.maxWords >= data.minWords, {
1713
1917
  error: "maxWords must be greater than or equal to minWords.",
1714
1918
  path: ["maxWords"]
1715
1919
  });
1716
1920
 
1717
1921
  // src/schemas/redacted.ts
1718
- import { z as z9 } from "zod/v4";
1922
+ import { z as z10 } from "zod/v4";
1719
1923
  var redactedBase = {
1720
1924
  /** Marker distinguishing a redacted projection from full activity data. */
1721
- redacted: z9.literal(true),
1925
+ redacted: z10.literal(true),
1722
1926
  /** Slot identity, carried through redaction so the client and the plan agree. */
1723
- slotKey: z9.string().min(1).optional(),
1724
- schemaVersion: z9.literal("1.0"),
1725
- id: z9.string().min(1),
1726
- title: z9.string().min(1),
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),
1727
1931
  media: RedactedMediaSchema.optional(),
1728
- passThreshold: z9.number().min(0).max(1).optional(),
1729
- locale: z9.string().optional(),
1730
- learningObjectives: z9.array(z9.string()).optional(),
1731
- difficultyLevel: z9.literal([1, 2, 3, 4, 5]).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()
1732
1937
  };
1733
- var RedactedMultipleChoiceOptionMediaSchema = z9.strictObject({
1734
- type: z9.enum(["image", "audio"]),
1735
- url: z9.string().min(1),
1736
- alt: z9.string().min(1).optional(),
1737
- captionsUrl: z9.string().min(1).optional()
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()
1738
1943
  });
1739
- var RedactedMultipleChoiceOptionSchema = z9.strictObject({
1740
- id: z9.string().min(1),
1741
- text: z9.string().min(1),
1944
+ var RedactedMultipleChoiceOptionSchema = z10.strictObject({
1945
+ id: z10.string().min(1),
1946
+ text: z10.string().min(1),
1742
1947
  media: RedactedMultipleChoiceOptionMediaSchema.optional()
1743
1948
  });
1744
- var RedactedMultipleChoiceDataSchema = z9.strictObject({
1949
+ var RedactedMultipleChoiceDataSchema = z10.strictObject({
1745
1950
  ...redactedBase,
1746
- type: z9.literal("multiple-choice"),
1747
- question: z9.string().min(1),
1748
- questionHtml: z9.string().optional(),
1749
- mode: z9.enum(["single", "multi"]),
1750
- options: z9.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
1751
- shuffle: z9.boolean().optional()
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()
1752
1957
  });
1753
- var RedactedBlankConfigSchema = z9.strictObject({
1754
- id: z9.string().min(1),
1755
- hint: z9.string().optional()
1958
+ var RedactedBlankConfigSchema = z10.strictObject({
1959
+ id: z10.string().min(1),
1960
+ hint: z10.string().optional()
1756
1961
  });
1757
- var RedactedFillInTheBlanksDataSchema = z9.strictObject({
1962
+ var RedactedFillInTheBlanksDataSchema = z10.strictObject({
1758
1963
  ...redactedBase,
1759
- type: z9.literal("fill-in-the-blanks"),
1760
- passage: z9.string().min(1),
1761
- passageHtml: z9.string().optional(),
1762
- blanks: z9.array(RedactedBlankConfigSchema).min(1)
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)
1763
1968
  });
1764
- var RedactedGapSelectChoiceSchema = z9.strictObject({
1765
- id: z9.string().min(1),
1766
- text: z9.string().min(1)
1969
+ var RedactedGapSelectChoiceSchema = z10.strictObject({
1970
+ id: z10.string().min(1),
1971
+ text: z10.string().min(1)
1767
1972
  });
1768
- var RedactedGapSelectBankSchema = z9.strictObject({
1769
- id: z9.string().min(1),
1770
- choices: z9.array(RedactedGapSelectChoiceSchema).min(2)
1973
+ var RedactedGapSelectBankSchema = z10.strictObject({
1974
+ id: z10.string().min(1),
1975
+ choices: z10.array(RedactedGapSelectChoiceSchema).min(2)
1771
1976
  });
1772
- var RedactedGapSelectGapSchema = z9.strictObject({
1773
- id: z9.string().min(1),
1774
- choices: z9.array(RedactedGapSelectChoiceSchema).min(2).optional(),
1775
- bankId: z9.string().min(1).optional()
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()
1776
1981
  });
1777
- var RedactedGapSelectDataSchema = z9.strictObject({
1982
+ var RedactedGapSelectDataSchema = z10.strictObject({
1778
1983
  ...redactedBase,
1779
- type: z9.literal("gap-select"),
1780
- passage: z9.string().min(1),
1781
- passageHtml: z9.string().optional(),
1782
- gaps: z9.array(RedactedGapSelectGapSchema).min(1),
1783
- banks: z9.array(RedactedGapSelectBankSchema).optional(),
1784
- presentation: z9.literal("dropdown").optional(),
1785
- shuffleChoices: z9.boolean().optional()
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()
1786
1991
  });
1787
- var RedactedWrittenResponseDataSchema = z9.strictObject({
1992
+ var RedactedWrittenResponseDataSchema = z10.strictObject({
1788
1993
  ...redactedBase,
1789
- type: z9.literal("written-response"),
1790
- prompt: z9.string(),
1791
- promptHtml: z9.string().optional(),
1792
- minWords: z9.number().int().min(0),
1793
- maxWords: z9.number().int().min(1),
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),
1794
1999
  rubric: RedactedWrittenResponseRubricSchema.optional(),
1795
- languageTarget: z9.string().optional()
2000
+ languageTarget: z10.string().optional()
1796
2001
  });
1797
- var RedactedDictationSlowMediaSchema = z9.strictObject({
1798
- type: z9.literal("audio"),
2002
+ var RedactedDictationSlowMediaSchema = z10.strictObject({
2003
+ type: z10.literal("audio"),
1799
2004
  url: MediaUrlSchema,
1800
- alt: z9.string().min(1).optional()
2005
+ alt: z10.string().min(1).optional()
1801
2006
  });
1802
- var RedactedDictationDataSchema = z9.strictObject({
2007
+ var RedactedDictationDataSchema = z10.strictObject({
1803
2008
  ...redactedBase,
1804
- type: z9.literal("dictation"),
2009
+ type: z10.literal("dictation"),
1805
2010
  slowMedia: RedactedDictationSlowMediaSchema.optional(),
1806
- hints: z9.strictObject({ mode: z9.literal("progressive-words") }).optional()
2011
+ hints: z10.strictObject({ mode: z10.literal("progressive-words") }).optional()
1807
2012
  }).check((ctx) => {
1808
2013
  const data = ctx.value;
1809
2014
  for (const captions of captionFieldsOf(data.media)) {
@@ -1831,23 +2036,23 @@ var RedactedDictationDataSchema = z9.strictObject({
1831
2036
  });
1832
2037
  }
1833
2038
  });
1834
- var RedactedReadAloudDataSchema = z9.strictObject({
2039
+ var RedactedReadAloudDataSchema = z10.strictObject({
1835
2040
  ...redactedBase,
1836
- type: z9.literal("read-aloud"),
1837
- locale: z9.string().regex(CANONICAL_LOCALE_RE),
1838
- instructions: z9.string().optional(),
1839
- referenceText: z9.string().min(1),
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),
1840
2045
  slowMedia: ReadAloudSlowMediaSchema.optional(),
1841
- recording: z9.strictObject({
1842
- maxSeconds: z9.number().gt(0).max(READ_ALOUD_MAX_SECONDS),
1843
- minSeconds: z9.number().min(0).optional(),
1844
- maxTakes: z9.number().int().min(1).max(READ_ALOUD_MAX_TAKES).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()
1845
2050
  }),
1846
- scoring: z9.strictObject({
1847
- dimensions: z9.array(
1848
- z9.strictObject({
1849
- name: z9.enum(READ_ALOUD_DIMENSIONS),
1850
- weight: z9.number().min(0).max(READ_ALOUD_MAX_DIMENSION_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)
1851
2056
  })
1852
2057
  ).min(1).max(READ_ALOUD_DIMENSIONS.length)
1853
2058
  })
@@ -1887,197 +2092,6 @@ var RedactedReadAloudDataSchema = z9.strictObject({
1887
2092
  }
1888
2093
  });
1889
2094
 
1890
- // src/scoring/dictation/edit-distance.ts
1891
- function codePoints(text) {
1892
- return Array.from(text, (character) => character.codePointAt(0));
1893
- }
1894
- function editDistance(a, b) {
1895
- if (a === b) {
1896
- return 0;
1897
- }
1898
- let left = codePoints(a);
1899
- let right = codePoints(b);
1900
- if (left.length < right.length) {
1901
- [left, right] = [right, left];
1902
- }
1903
- if (right.length === 0) {
1904
- return left.length;
1905
- }
1906
- let previous = new Uint32Array(right.length + 1);
1907
- let current = new Uint32Array(right.length + 1);
1908
- for (let j = 0; j <= right.length; j += 1) {
1909
- previous[j] = j;
1910
- }
1911
- for (let i = 1; i <= left.length; i += 1) {
1912
- current[0] = i;
1913
- const leftPoint = left[i - 1];
1914
- for (let j = 1; j <= right.length; j += 1) {
1915
- const deletion = previous[j] + 1;
1916
- const insertion = current[j - 1] + 1;
1917
- const substitution = previous[j - 1] + (leftPoint === right[j - 1] ? 0 : 1);
1918
- current[j] = Math.min(deletion, insertion, substitution);
1919
- }
1920
- [previous, current] = [current, previous];
1921
- }
1922
- return previous[right.length];
1923
- }
1924
- function similarity(a, b) {
1925
- const max = Math.max(codePoints(a).length, codePoints(b).length);
1926
- return max === 0 ? 0 : (max - editDistance(a, b)) / max;
1927
- }
1928
-
1929
- // src/scoring/dictation/align.ts
1930
- function dictationReferenceWords(data) {
1931
- return rawCandidates(data).map(
1932
- (candidate) => tokensOf(normalizeCandidate(candidate, data)).map((word, index) => ({
1933
- itemId: `w${index + 1}`,
1934
- word
1935
- }))
1936
- );
1937
- }
1938
- function alignDictation(data, text) {
1939
- const raw = typeof text === "string" ? text : "";
1940
- const capped = truncateCodePoints(raw, DICTATION_MAX_TEXT_LENGTH);
1941
- const { text: normalized, cut } = normalizeDictationTextBounded(capped, data.tolerance);
1942
- const attempt = cutAt(normalized, DICTATION_MAX_TEXT_LENGTH);
1943
- const truncated = cut || capped.length < raw.length || attempt.length < normalized.length;
1944
- const candidates = [];
1945
- rawCandidates(data).forEach((candidate, index) => {
1946
- const candidateText = normalizeCandidate(candidate, data);
1947
- if (candidateText !== "") {
1948
- candidates.push({ index, text: candidateText });
1949
- }
1950
- });
1951
- if (candidates.length === 0) {
1952
- return { candidateIndex: -1, reference: "", attempt, truncated, similarity: 0, words: [] };
1953
- }
1954
- let chosen = candidates[0];
1955
- let best = similarity(chosen.text, attempt);
1956
- for (const candidate of candidates.slice(1)) {
1957
- const candidateSimilarity = similarity(candidate.text, attempt);
1958
- if (candidateSimilarity > best) {
1959
- best = candidateSimilarity;
1960
- chosen = candidate;
1961
- }
1962
- }
1963
- return {
1964
- candidateIndex: chosen.index,
1965
- reference: chosen.text,
1966
- attempt,
1967
- truncated,
1968
- similarity: best,
1969
- words: alignWords(tokensOf(chosen.text), tokensOf(attempt))
1970
- };
1971
- }
1972
- function rawCandidates(data) {
1973
- return [
1974
- data.transcript,
1975
- ...Array.isArray(data.acceptedTranscripts) ? data.acceptedTranscripts.slice(0, DICTATION_MAX_ACCEPTED_TRANSCRIPTS) : []
1976
- ];
1977
- }
1978
- function normalizeCandidate(candidate, data) {
1979
- return typeof candidate === "string" ? cutAt(
1980
- normalizeDictationText(
1981
- truncateCodePoints(candidate, DICTATION_MAX_TRANSCRIPT_LENGTH),
1982
- data.tolerance
1983
- ),
1984
- DICTATION_MAX_TRANSCRIPT_LENGTH
1985
- ) : "";
1986
- }
1987
- function cutAt(normalized, max) {
1988
- const cut = truncateCodePoints(normalized, max);
1989
- return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
1990
- }
1991
- function tokensOf(normalized) {
1992
- return normalized === "" ? [] : normalized.split(" ");
1993
- }
1994
- var STEP = {
1995
- /** A reference element paired with an attempt element — equal or substituted. */
1996
- pair: 0,
1997
- /** A reference element the attempt lacks. */
1998
- missing: 1,
1999
- /** An attempt element the reference lacks. */
2000
- extra: 2
2001
- };
2002
- function alignSequences(reference, attempt) {
2003
- const rows = reference.length;
2004
- const columns = attempt.length;
2005
- const width = columns + 1;
2006
- const steps = new Uint8Array((rows + 1) * width);
2007
- let previous = new Uint32Array(width);
2008
- let current = new Uint32Array(width);
2009
- for (let j2 = 0; j2 <= columns; j2 += 1) {
2010
- previous[j2] = j2;
2011
- steps[j2] = STEP.extra;
2012
- }
2013
- for (let i2 = 1; i2 <= rows; i2 += 1) {
2014
- current[0] = i2;
2015
- steps[i2 * width] = STEP.missing;
2016
- const referenceItem = reference[i2 - 1];
2017
- for (let j2 = 1; j2 <= columns; j2 += 1) {
2018
- const paired = previous[j2 - 1] + (referenceItem === attempt[j2 - 1] ? 0 : 1);
2019
- const missing = previous[j2] + 1;
2020
- const extra = current[j2 - 1] + 1;
2021
- const cost = Math.min(paired, missing, extra);
2022
- current[j2] = cost;
2023
- steps[i2 * width + j2] = paired === cost ? STEP.pair : missing === cost ? STEP.missing : STEP.extra;
2024
- }
2025
- [previous, current] = [current, previous];
2026
- }
2027
- const path = [];
2028
- let i = rows;
2029
- let j = columns;
2030
- while (i > 0 || j > 0) {
2031
- const step = steps[i * width + j];
2032
- if (step === STEP.pair) {
2033
- i -= 1;
2034
- j -= 1;
2035
- } else if (step === STEP.missing) {
2036
- i -= 1;
2037
- } else {
2038
- j -= 1;
2039
- }
2040
- path.push({ step, referenceIndex: i, attemptIndex: j });
2041
- }
2042
- return path.reverse();
2043
- }
2044
- function alignWords(reference, attempt) {
2045
- const words = [];
2046
- let ordinal = 0;
2047
- for (const { step, referenceIndex, attemptIndex } of alignSequences(reference, attempt)) {
2048
- if (step === STEP.extra) {
2049
- words.push({
2050
- reference: "",
2051
- attempt: attempt[attemptIndex],
2052
- similarity: 0,
2053
- status: "extra"
2054
- });
2055
- continue;
2056
- }
2057
- ordinal += 1;
2058
- const referenceWord = reference[referenceIndex];
2059
- if (step === STEP.missing) {
2060
- words.push({
2061
- itemId: `w${ordinal}`,
2062
- reference: referenceWord,
2063
- attempt: "",
2064
- similarity: 0,
2065
- status: "missing"
2066
- });
2067
- continue;
2068
- }
2069
- const attemptWord = attempt[attemptIndex];
2070
- words.push({
2071
- itemId: `w${ordinal}`,
2072
- reference: referenceWord,
2073
- attempt: attemptWord,
2074
- similarity: similarity(referenceWord, attemptWord),
2075
- status: referenceWord === attemptWord ? "correct" : "incorrect"
2076
- });
2077
- }
2078
- return words;
2079
- }
2080
-
2081
2095
  // src/scoring/text-match.ts
2082
2096
  var COMBINING_MARKS_RE = /\p{M}/gu;
2083
2097
  var PUNCTUATION_RE2 = /\p{P}/gu;
@@ -2202,6 +2216,7 @@ var DRAFT_ISSUE_SEVERITY = {
2202
2216
  scoring_strategy_required: "incomplete",
2203
2217
  pass_threshold_invalid: "invalid",
2204
2218
  difficulty_level_invalid: "invalid",
2219
+ ai_permissions_invalid: "invalid",
2205
2220
  feedback_empty: "incomplete",
2206
2221
  redacted_data: "invalid",
2207
2222
  media_type_required: "incomplete",
@@ -2436,6 +2451,30 @@ function checkSharedOptional(draft) {
2436
2451
  )
2437
2452
  );
2438
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
+ }
2439
2478
  const feedback = draft.feedback;
2440
2479
  if (isRecord(feedback)) {
2441
2480
  for (const key of ["correct", "incorrect"]) {
@@ -4075,7 +4114,11 @@ var SHARED_PUBLIC_FIELDS = {
4075
4114
  passThreshold: "public",
4076
4115
  locale: "public",
4077
4116
  learningObjectives: "public",
4078
- 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" }
4079
4122
  };
4080
4123
  var FEEDBACK_FIELD_POLICY = {
4081
4124
  correct: "answer-key",
@@ -4348,10 +4391,18 @@ registerActivityType(dictationType);
4348
4391
  registerActivityType(readAloudType);
4349
4392
 
4350
4393
  export {
4351
- ActivitySchemaError,
4352
- UnknownActivityTypeError,
4353
- RedactedScoringError,
4354
- DeferredScoringError,
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,
4355
4406
  withValidationScope,
4356
4407
  DICTATION_MAX_TRANSCRIPT_LENGTH,
4357
4408
  DICTATION_MAX_TEXT_LENGTH,
@@ -4360,13 +4411,14 @@ export {
4360
4411
  DICTATION_MAX_ACCEPTED_TRANSCRIPTS,
4361
4412
  codePointLength,
4362
4413
  normalizeDictationText,
4363
- FeedbackSchema,
4364
- MediaUrlSchema,
4365
- NativeControlHintSchema,
4366
- MediaPlaybackSchema,
4367
- MediaTrackSchema,
4368
- MediaSchema,
4369
- RedactedMediaSchema,
4414
+ dictationReferenceWords,
4415
+ alignDictation,
4416
+ STEP,
4417
+ alignSequences,
4418
+ ActivitySchemaError,
4419
+ UnknownActivityTypeError,
4420
+ RedactedScoringError,
4421
+ DeferredScoringError,
4370
4422
  DictationSlowMediaSchema,
4371
4423
  DictationEquivalenceSchema,
4372
4424
  DictationToleranceSchema,
@@ -4384,9 +4436,6 @@ export {
4384
4436
  covers,
4385
4437
  pathKey,
4386
4438
  coveredPathsOf,
4387
- TextMatchPolicySchema,
4388
- BlankConfigSchema,
4389
- FillInTheBlanksDataSchema,
4390
4439
  MultipleChoiceOptionMediaSchema,
4391
4440
  MultipleChoiceOptionSchema,
4392
4441
  MultipleChoiceDataSchema,
@@ -4420,10 +4469,6 @@ export {
4420
4469
  RedactedDictationSlowMediaSchema,
4421
4470
  RedactedDictationDataSchema,
4422
4471
  RedactedReadAloudDataSchema,
4423
- dictationReferenceWords,
4424
- alignDictation,
4425
- STEP,
4426
- alignSequences,
4427
4472
  levenshteinDistance,
4428
4473
  matchText,
4429
4474
  defineActivityType,
@@ -4438,4 +4483,4 @@ export {
4438
4483
  dictationType,
4439
4484
  readAloudType
4440
4485
  };
4441
- //# sourceMappingURL=chunk-IRUD73IQ.js.map
4486
+ //# sourceMappingURL=chunk-D3GP5MET.js.map