@intellectif/lk-core 1.1.0 → 1.2.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 (64) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -1
  3. package/dist/{activity-MBOYLBjR.d.cts → activity-D705CXng.d.cts} +324 -2
  4. package/dist/{activity-MBOYLBjR.d.ts → activity-D705CXng.d.ts} +324 -2
  5. package/dist/ai-check.cjs +174 -12
  6. package/dist/ai-check.cjs.map +1 -1
  7. package/dist/ai-check.d.cts +15 -3
  8. package/dist/ai-check.d.ts +15 -3
  9. package/dist/ai-check.js +166 -4
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{chunk-TJJFKKH2.cjs → chunk-46BXIGLG.cjs} +4 -4
  12. package/dist/{chunk-TJJFKKH2.cjs.map → chunk-46BXIGLG.cjs.map} +1 -1
  13. package/dist/{chunk-VSH7ECQL.cjs → chunk-4YANYTFN.cjs} +43 -43
  14. package/dist/{chunk-VSH7ECQL.cjs.map → chunk-4YANYTFN.cjs.map} +1 -1
  15. package/dist/chunk-7KOWU4RO.js +2207 -0
  16. package/dist/chunk-7KOWU4RO.js.map +1 -0
  17. package/dist/{chunk-DSDXL3JO.cjs → chunk-7THXD7AN.cjs} +657 -197
  18. package/dist/chunk-7THXD7AN.cjs.map +1 -0
  19. package/dist/chunk-CYCTIXUN.cjs +118 -0
  20. package/dist/chunk-CYCTIXUN.cjs.map +1 -0
  21. package/dist/chunk-CYPA5BW5.js +118 -0
  22. package/dist/chunk-CYPA5BW5.js.map +1 -0
  23. package/dist/{chunk-SJVRUPJT.js → chunk-JTNDIBZO.js} +2 -2
  24. package/dist/{chunk-XS6U2ARZ.js → chunk-MV2N2WNS.js} +6 -112
  25. package/dist/chunk-MV2N2WNS.js.map +1 -0
  26. package/dist/chunk-MXI26H62.cjs +2207 -0
  27. package/dist/chunk-MXI26H62.cjs.map +1 -0
  28. package/dist/{chunk-FBKTION6.js → chunk-QS7B42ZR.js} +640 -180
  29. package/dist/chunk-QS7B42ZR.js.map +1 -0
  30. package/dist/{chunk-IARLJ5MW.js → chunk-QX2ERL72.js} +2 -2
  31. package/dist/{chunk-2JT2WUQ2.cjs → chunk-ZWXQSKCX.cjs} +14 -120
  32. package/dist/chunk-ZWXQSKCX.cjs.map +1 -0
  33. package/dist/{index-DlcqkM_L.d.ts → index-BChYtrEe.d.ts} +1 -1
  34. package/dist/{index-DlCW0XaQ.d.cts → index-VXt_v3YI.d.cts} +1 -1
  35. package/dist/index.cjs +68 -593
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +245 -60
  38. package/dist/index.d.ts +245 -60
  39. package/dist/index.js +68 -593
  40. package/dist/index.js.map +1 -1
  41. package/dist/schemas.cjs +5 -3
  42. package/dist/schemas.cjs.map +1 -1
  43. package/dist/schemas.d.cts +2 -2
  44. package/dist/schemas.d.ts +2 -2
  45. package/dist/schemas.js +5 -3
  46. package/dist/scoring.cjs +3 -3
  47. package/dist/scoring.d.cts +2 -2
  48. package/dist/scoring.d.ts +2 -2
  49. package/dist/scoring.js +2 -2
  50. package/dist/xapi.cjs +3 -3
  51. package/dist/xapi.d.cts +1 -1
  52. package/dist/xapi.d.ts +1 -1
  53. package/dist/xapi.js +2 -2
  54. package/package.json +1 -1
  55. package/dist/chunk-2JT2WUQ2.cjs.map +0 -1
  56. package/dist/chunk-DSDXL3JO.cjs.map +0 -1
  57. package/dist/chunk-FBKTION6.js.map +0 -1
  58. package/dist/chunk-LDFQUZII.cjs +0 -950
  59. package/dist/chunk-LDFQUZII.cjs.map +0 -1
  60. package/dist/chunk-NQHETFDA.js +0 -950
  61. package/dist/chunk-NQHETFDA.js.map +0 -1
  62. package/dist/chunk-XS6U2ARZ.js.map +0 -1
  63. /package/dist/{chunk-SJVRUPJT.js.map → chunk-JTNDIBZO.js.map} +0 -0
  64. /package/dist/{chunk-IARLJ5MW.js.map → chunk-QX2ERL72.js.map} +0 -0
@@ -0,0 +1,2207 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+
6
+
7
+ var _chunk4YANYTFNcjs = require('./chunk-4YANYTFN.cjs');
8
+
9
+
10
+
11
+
12
+
13
+ var _chunkZWXQSKCXcjs = require('./chunk-ZWXQSKCX.cjs');
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+ var _chunk7THXD7ANcjs = require('./chunk-7THXD7AN.cjs');
42
+
43
+ // src/ai-text.ts
44
+ function cleanText(text2) {
45
+ return text2.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u2028\u2029]/g, "").replace(/\n{3,}/g, "\n\n").trim();
46
+ }
47
+ function provenanceOf(raw) {
48
+ if (typeof raw !== "object" || raw === null) {
49
+ return void 0;
50
+ }
51
+ const kept = {};
52
+ for (const key of ["model", "promptHash", "generatedAt"]) {
53
+ const value = raw[key];
54
+ if (typeof value === "string" && value.length > 0 && value.length <= 200) {
55
+ kept[key] = value;
56
+ }
57
+ }
58
+ return Object.keys(kept).length > 0 ? kept : void 0;
59
+ }
60
+ function usageOf(raw) {
61
+ if (typeof raw !== "object" || raw === null) {
62
+ return void 0;
63
+ }
64
+ const kept = {};
65
+ for (const key of ["promptTokens", "completionTokens", "costUsd"]) {
66
+ const value = raw[key];
67
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
68
+ kept[key] = value;
69
+ }
70
+ }
71
+ return Object.keys(kept).length > 0 ? kept : void 0;
72
+ }
73
+ var QUOTE_FOLD = {
74
+ "\u2018": "'",
75
+ "\u2019": "'",
76
+ "\u201B": "'",
77
+ "\u2032": "'",
78
+ "\u201C": '"',
79
+ "\u201D": '"',
80
+ "\u201E": '"',
81
+ "\u2033": '"'
82
+ };
83
+ function folded(text2) {
84
+ let chars = "";
85
+ const from = [];
86
+ let space = false;
87
+ for (let at = 0; at < text2.length; at += 1) {
88
+ const char = text2[at];
89
+ if (/\s/u.test(char)) {
90
+ if (!space) {
91
+ chars += " ";
92
+ from.push(at);
93
+ space = true;
94
+ }
95
+ continue;
96
+ }
97
+ space = false;
98
+ chars += _nullishCoalesce(QUOTE_FOLD[char], () => ( char));
99
+ from.push(at);
100
+ }
101
+ return { chars, from };
102
+ }
103
+ var needle = (quote) => folded(quote).chars.trim();
104
+
105
+ // src/ai.ts
106
+ var AI_EXPLANATION_TYPES = [
107
+ "multiple-choice",
108
+ "fill-in-the-blanks",
109
+ "gap-select",
110
+ "dictation"
111
+ ];
112
+ var AI_HINT_TYPES = [
113
+ "multiple-choice",
114
+ "fill-in-the-blanks",
115
+ "gap-select"
116
+ ];
117
+ var AI_TEXT_MAX_LENGTH = 2e3;
118
+ function aiSupports(activityType, feature) {
119
+ if (feature === "writing-feedback") {
120
+ return activityType === "written-response";
121
+ }
122
+ if (feature === "pronunciation-coaching") {
123
+ return activityType === "read-aloud";
124
+ }
125
+ const types = feature === "hint" ? AI_HINT_TYPES : AI_EXPLANATION_TYPES;
126
+ return types.includes(activityType);
127
+ }
128
+ function aiAllowedByContent(data, feature) {
129
+ const permissions = data.ai;
130
+ if (typeof permissions !== "object" || permissions === null) {
131
+ return true;
132
+ }
133
+ const key = feature === "hint" ? "hints" : "explanations";
134
+ return permissions[key] !== false;
135
+ }
136
+ function aiGradeOf(result) {
137
+ const ratio = result.maxScore > 0 ? result.score / result.maxScore : 0;
138
+ let category = "partly-correct";
139
+ if (ratio >= 1 - 1e-9) {
140
+ category = "correct";
141
+ } else if (ratio <= 1e-9) {
142
+ category = "incorrect";
143
+ }
144
+ return { score: result.score, maxScore: result.maxScore, passed: result.passed, category };
145
+ }
146
+ var legacyCorrect = (detail) => detail.correct === true;
147
+ function detailRight(detail) {
148
+ const outcome = detail.outcome;
149
+ return outcome === void 0 ? legacyCorrect(detail) : outcome === "correct";
150
+ }
151
+ function isAnswerOption(detail, chosen) {
152
+ switch (detail.outcome) {
153
+ case "correct":
154
+ case "incorrect-omission":
155
+ return true;
156
+ case "incorrect":
157
+ case "correct-omission":
158
+ return false;
159
+ default:
160
+ return chosen ? legacyCorrect(detail) : !legacyCorrect(detail);
161
+ }
162
+ }
163
+ var firstString = (value) => Array.isArray(value) ? value[0] : value;
164
+ var listOf = (value) => value === void 0 ? null : Array.isArray(value) ? [...value] : [value];
165
+ function emptyResponse(type) {
166
+ switch (type) {
167
+ case "multiple-choice":
168
+ return { type: "multiple-choice", selectedOptionIds: [] };
169
+ case "fill-in-the-blanks":
170
+ return { type: "fill-in-the-blanks", answers: {} };
171
+ case "gap-select":
172
+ return { type: "gap-select", selections: {} };
173
+ case "dictation":
174
+ return { type: "dictation", text: "" };
175
+ default:
176
+ return null;
177
+ }
178
+ }
179
+ var base = (data) => ({
180
+ activityId: data.id,
181
+ title: data.title,
182
+ ...typeof data.locale === "string" ? { locale: data.locale } : {}
183
+ });
184
+ function buildAiFacts(data, response, details) {
185
+ const byId = new Map((_nullishCoalesce(details, () => ( []))).map((detail) => [detail.itemId, detail]));
186
+ const full = data.redacted !== true;
187
+ switch (data.type) {
188
+ case "multiple-choice": {
189
+ const item = data;
190
+ const chosen = new Set(
191
+ _optionalChain([response, 'optionalAccess', _ => _.type]) === "multiple-choice" ? response.selectedOptionIds : []
192
+ );
193
+ return {
194
+ ...base(data),
195
+ activityType: "multiple-choice",
196
+ question: item.question,
197
+ mode: item.mode,
198
+ options: item.options.map((option) => {
199
+ const detail = byId.get(option.id);
200
+ const wasChosen = chosen.has(option.id);
201
+ let correct = null;
202
+ if (full && typeof option.isCorrect === "boolean") {
203
+ correct = option.isCorrect;
204
+ } else if (detail !== void 0) {
205
+ correct = isAnswerOption(detail, wasChosen);
206
+ }
207
+ return {
208
+ id: option.id,
209
+ text: option.text,
210
+ chosen: wasChosen,
211
+ correct,
212
+ ...typeof option.feedback === "string" ? { feedback: option.feedback } : {}
213
+ };
214
+ })
215
+ };
216
+ }
217
+ case "fill-in-the-blanks": {
218
+ const item = data;
219
+ const answers = _optionalChain([response, 'optionalAccess', _2 => _2.type]) === "fill-in-the-blanks" ? response.answers : {};
220
+ const blanks = new Map(item.blanks.map((blank) => [blank.id, blank]));
221
+ return {
222
+ ...base(data),
223
+ activityType: "fill-in-the-blanks",
224
+ passage: _chunk7THXD7ANcjs.numberedPassage.call(void 0, item.passage),
225
+ blanks: _chunk7THXD7ANcjs.placeholderOrder.call(void 0, item.passage).map((id, index) => {
226
+ const blank = blanks.get(id);
227
+ const detail = byId.get(id);
228
+ const accepted = full && Array.isArray(_optionalChain([blank, 'optionalAccess', _3 => _3.acceptedAnswers])) ? [...blank.acceptedAnswers] : listOf(_optionalChain([detail, 'optionalAccess', _4 => _4.correctResponse]));
229
+ return {
230
+ id,
231
+ position: index + 1,
232
+ typed: typeof answers[id] === "string" ? answers[id] : "",
233
+ accepted,
234
+ correct: detail === void 0 ? null : detailRight(detail),
235
+ ...typeof _optionalChain([blank, 'optionalAccess', _5 => _5.hint]) === "string" ? { hint: blank.hint } : {},
236
+ ...typeof _optionalChain([blank, 'optionalAccess', _6 => _6.feedback]) === "string" ? { feedback: blank.feedback } : {}
237
+ };
238
+ })
239
+ };
240
+ }
241
+ case "gap-select": {
242
+ const item = data;
243
+ const selections = _optionalChain([response, 'optionalAccess', _7 => _7.type]) === "gap-select" ? response.selections : {};
244
+ const gaps = new Map(item.gaps.map((gap) => [gap.id, gap]));
245
+ const banks = new Map((_nullishCoalesce(item.banks, () => ( []))).map((bank) => [bank.id, bank]));
246
+ return {
247
+ ...base(data),
248
+ activityType: "gap-select",
249
+ passage: _chunk7THXD7ANcjs.numberedPassage.call(void 0, item.passage),
250
+ gaps: _chunk7THXD7ANcjs.placeholderOrder.call(void 0, item.passage).map((id, index) => {
251
+ const gap = gaps.get(id);
252
+ const choices = _nullishCoalesce(_optionalChain([gap, 'optionalAccess', _8 => _8.choices]), () => ( (_optionalChain([gap, 'optionalAccess', _9 => _9.bankId]) === void 0 ? [] : _nullishCoalesce(_optionalChain([banks, 'access', _10 => _10.get, 'call', _11 => _11(gap.bankId), 'optionalAccess', _12 => _12.choices]), () => ( [])))));
253
+ const textOf = (choiceId) => choiceId === void 0 ? null : _nullishCoalesce(_optionalChain([choices, 'access', _13 => _13.find, 'call', _14 => _14((choice) => choice.id === choiceId), 'optionalAccess', _15 => _15.text]), () => ( null));
254
+ const detail = byId.get(id);
255
+ const answerId = full && typeof _optionalChain([gap, 'optionalAccess', _16 => _16.correctChoiceId]) === "string" ? gap.correctChoiceId : firstString(_optionalChain([detail, 'optionalAccess', _17 => _17.correctResponse]));
256
+ return {
257
+ id,
258
+ position: index + 1,
259
+ choices: choices.map((choice) => choice.text),
260
+ chosen: textOf(selections[id]),
261
+ answer: textOf(answerId),
262
+ correct: detail === void 0 ? null : detailRight(detail),
263
+ ...typeof _optionalChain([gap, 'optionalAccess', _18 => _18.feedback]) === "string" ? { feedback: gap.feedback } : {}
264
+ };
265
+ })
266
+ };
267
+ }
268
+ case "dictation": {
269
+ const item = data;
270
+ const typed = _optionalChain([response, 'optionalAccess', _19 => _19.type]) === "dictation" ? response.text : "";
271
+ const hasKey = full && typeof item.transcript === "string";
272
+ let words = null;
273
+ if (hasKey) {
274
+ words = _chunk7THXD7ANcjs.alignDictation.call(void 0, item, typed).words.map((word) => ({
275
+ expected: word.reference,
276
+ typed: word.attempt,
277
+ status: word.status
278
+ }));
279
+ } else if (details !== null && details.length > 0) {
280
+ words = details.map((detail) => {
281
+ const expected = _nullishCoalesce(firstString(detail.correctResponse), () => ( ""));
282
+ const attempt = _nullishCoalesce(firstString(detail.learnerResponse), () => ( ""));
283
+ let status = "incorrect";
284
+ if (detailRight(detail)) {
285
+ status = "correct";
286
+ } else if (attempt === "") {
287
+ status = "missing";
288
+ } else if (expected === "") {
289
+ status = "extra";
290
+ }
291
+ return { expected, typed: attempt, status };
292
+ });
293
+ }
294
+ return {
295
+ ...base(data),
296
+ activityType: "dictation",
297
+ transcript: hasKey ? item.transcript : null,
298
+ typed,
299
+ words
300
+ };
301
+ }
302
+ default:
303
+ return null;
304
+ }
305
+ }
306
+ function gradeLocally(data, response) {
307
+ if (data.redacted === true) {
308
+ return null;
309
+ }
310
+ try {
311
+ const result = _chunk4YANYTFNcjs.score.call(void 0, data.type, data, response);
312
+ return { details: result.details, grade: aiGradeOf(result) };
313
+ } catch (e) {
314
+ return null;
315
+ }
316
+ }
317
+ function aiExplanationRequest(input) {
318
+ const { data, response, outcome, learnerLocale } = input;
319
+ if (!aiSupports(data.type, "explanation") || !aiAllowedByContent(data, "explanation")) {
320
+ return null;
321
+ }
322
+ let details = null;
323
+ let grade = null;
324
+ if (_optionalChain([outcome, 'optionalAccess', _20 => _20.status]) === "scored") {
325
+ const { score: recorded, maxScore, passed } = outcome;
326
+ if (!_chunk4YANYTFNcjs.isGradeInRange.call(void 0, recorded, maxScore)) {
327
+ return null;
328
+ }
329
+ details = outcome.details;
330
+ grade = aiGradeOf({ score: recorded, maxScore, passed });
331
+ } else if (response !== null) {
332
+ const local = gradeLocally(data, response);
333
+ if (local !== null) {
334
+ details = local.details;
335
+ grade = local.grade;
336
+ }
337
+ }
338
+ if (grade === null) {
339
+ return null;
340
+ }
341
+ const facts = buildAiFacts(data, response, details);
342
+ if (facts === null) {
343
+ return null;
344
+ }
345
+ return {
346
+ feature: "explanation",
347
+ facts,
348
+ grade,
349
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
350
+ };
351
+ }
352
+ function aiHintRequest(input) {
353
+ const { data, previousHints, learnerLocale } = input;
354
+ if (!aiSupports(data.type, "hint") || !aiAllowedByContent(data, "hint") || data.redacted === true) {
355
+ return null;
356
+ }
357
+ const response = _nullishCoalesce(input.response, () => ( emptyResponse(data.type)));
358
+ const local = response === null ? null : gradeLocally(data, response);
359
+ const facts = buildAiFacts(data, response, _nullishCoalesce(_optionalChain([local, 'optionalAccess', _21 => _21.details]), () => ( null)));
360
+ if (facts === null) {
361
+ return null;
362
+ }
363
+ return {
364
+ feature: "hint",
365
+ facts,
366
+ hintNumber: previousHints.length + 1,
367
+ previousHints: [...previousHints],
368
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
369
+ };
370
+ }
371
+ var VERDICTS = ["correct", "partly-correct", "incorrect"];
372
+ function readAiTextResult(raw) {
373
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
374
+ return { ok: false, refusal: "malformed" };
375
+ }
376
+ const record = raw;
377
+ if (typeof record.text !== "string") {
378
+ return { ok: false, refusal: "malformed" };
379
+ }
380
+ if (record.verdict !== void 0 && !VERDICTS.includes(record.verdict)) {
381
+ return { ok: false, refusal: "malformed" };
382
+ }
383
+ const text2 = cleanText(record.text);
384
+ if (text2 === "") {
385
+ return { ok: false, refusal: "empty" };
386
+ }
387
+ if ([...text2].length > AI_TEXT_MAX_LENGTH) {
388
+ return { ok: false, refusal: "too-long" };
389
+ }
390
+ const provenance = provenanceOf(record.provenance);
391
+ const usage = usageOf(record.usage);
392
+ return {
393
+ ok: true,
394
+ result: {
395
+ text: text2,
396
+ ...record.verdict !== void 0 ? { verdict: record.verdict } : {},
397
+ ...provenance !== void 0 ? { provenance } : {},
398
+ ...usage !== void 0 ? { usage } : {}
399
+ }
400
+ };
401
+ }
402
+ function checkAiExplanation(raw, request) {
403
+ const read = readAiTextResult(raw);
404
+ if (!read.ok) {
405
+ return read;
406
+ }
407
+ if (read.result.verdict !== void 0 && read.result.verdict !== request.grade.category) {
408
+ return { ok: false, refusal: "contradicts-grade" };
409
+ }
410
+ return read;
411
+ }
412
+ function checkAiHint(raw, request) {
413
+ const read = readAiTextResult(raw);
414
+ if (!read.ok) {
415
+ return read;
416
+ }
417
+ if (hintRevealsAnswer(request.facts, read.result.text)) {
418
+ return { ok: false, refusal: "reveals-answer" };
419
+ }
420
+ return read;
421
+ }
422
+ function hintRevealsAnswer(facts, hint) {
423
+ const reveals = (answer, passage, position) => _chunk7THXD7ANcjs.revealsAnswer.call(void 0, hint, answer, passage, position);
424
+ switch (facts.activityType) {
425
+ case "multiple-choice":
426
+ return facts.options.some((option) => option.correct === true && reveals(option.text));
427
+ case "fill-in-the-blanks":
428
+ return facts.blanks.some(
429
+ (blank) => (_nullishCoalesce(blank.accepted, () => ( []))).some((answer) => reveals(answer, facts.passage, blank.position))
430
+ );
431
+ case "gap-select":
432
+ return facts.gaps.some(
433
+ (gap) => gap.answer !== null && reveals(gap.answer, facts.passage, gap.position)
434
+ );
435
+ case "dictation":
436
+ return false;
437
+ default:
438
+ return false;
439
+ }
440
+ }
441
+
442
+ // src/ai-coaching.ts
443
+ var AI_COACHING_MAX_WORDS = 20;
444
+ var AI_COACHING_MAX_TIP_LENGTH = 500;
445
+ var MAX_SYMBOL_LENGTH = 16;
446
+ var DIMENSIONS = ["accuracy", "fluency", "completeness", "prosody"];
447
+ var hundredths = (value) => Math.round(value * 1e4) / 100;
448
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
449
+ function soundsOf(word) {
450
+ const sounds = [];
451
+ for (const phoneme of _nullishCoalesce(_optionalChain([word, 'optionalAccess', _22 => _22.phonemes]), () => ( []))) {
452
+ if (typeof phoneme.symbol !== "string") {
453
+ continue;
454
+ }
455
+ sounds.push({
456
+ symbol: phoneme.symbol,
457
+ ...phoneme.accuracy !== void 0 ? { accuracy: phoneme.accuracy } : {},
458
+ ...phoneme.heardAs !== void 0 && phoneme.heardAs.length > 0 ? { heardAs: phoneme.heardAs.map(({ symbol, score: score2 }) => ({ symbol, score: score2 })) } : {}
459
+ });
460
+ }
461
+ return sounds.length > 0 ? sounds : void 0;
462
+ }
463
+ function fromAssessment(item, assessment) {
464
+ const words = _chunk4YANYTFNcjs.alignReadAloud.call(void 0, item, assessment).map((entry) => {
465
+ const sounds = entry.itemId !== void 0 && entry.wordIndex !== void 0 ? soundsOf(assessment.words[entry.wordIndex]) : void 0;
466
+ return {
467
+ ...entry.itemId !== void 0 ? { itemId: entry.itemId } : {},
468
+ word: entry.reference,
469
+ heard: entry.heard,
470
+ state: entry.state,
471
+ ...entry.accuracy !== void 0 ? { accuracy: entry.accuracy } : {},
472
+ ...sounds !== void 0 ? { sounds } : {}
473
+ };
474
+ });
475
+ const scores = {};
476
+ for (const dimension of DIMENSIONS) {
477
+ const value = assessment.scores[dimension];
478
+ if (value !== void 0) {
479
+ scores[dimension] = value;
480
+ }
481
+ }
482
+ return {
483
+ assessor: assessment.assessor.kind,
484
+ ...assessment.phonemeAlphabet !== void 0 ? { phonemeAlphabet: assessment.phonemeAlphabet } : {},
485
+ scores,
486
+ words
487
+ };
488
+ }
489
+ var marksThisText = (item, assessment) => _chunk4YANYTFNcjs.validateSpeechAssessment.call(void 0, assessment).success && assessment.status === "assessed" && assessment.task === "scripted" && assessment.locale === item.locale && assessment.referenceText === item.referenceText;
490
+ var STATE_OF_OUTCOME = {
491
+ correct: "correct",
492
+ incorrect: "mispronounced",
493
+ "incorrect-omission": "omitted"
494
+ };
495
+ function fromGrade(grade) {
496
+ if (!Array.isArray(grade.details) || grade.details.length === 0) {
497
+ return null;
498
+ }
499
+ const words = [];
500
+ for (const detail of grade.details) {
501
+ const state = STATE_OF_OUTCOME[detail.outcome];
502
+ if (state === void 0 || typeof detail.itemId !== "string") {
503
+ continue;
504
+ }
505
+ const text2 = (value) => Array.isArray(value) ? value.join(" ") : value;
506
+ words.push({
507
+ itemId: detail.itemId,
508
+ word: text2(detail.correctResponse),
509
+ heard: text2(detail.learnerResponse),
510
+ state,
511
+ ...state !== "omitted" && isFiniteNumber(detail.score) ? { accuracy: hundredths(detail.score) } : {}
512
+ });
513
+ }
514
+ if (words.length === 0) {
515
+ return null;
516
+ }
517
+ const scores = {};
518
+ for (const criterion of _nullishCoalesce(grade.criteria, () => ( []))) {
519
+ const dimension = DIMENSIONS.find((name) => name === criterion.name);
520
+ if (dimension !== void 0 && isFiniteNumber(criterion.score) && isFiniteNumber(criterion.maxScore) && criterion.maxScore > 0) {
521
+ scores[dimension] = hundredths(criterion.score / criterion.maxScore);
522
+ }
523
+ }
524
+ const kind = _optionalChain([grade, 'access', _23 => _23.grader, 'optionalAccess', _24 => _24.kind]);
525
+ return {
526
+ assessor: kind === "auto" || kind === "ai" || kind === "human" ? kind : "unknown",
527
+ scores,
528
+ words
529
+ };
530
+ }
531
+ function aiCoachingRequest(input) {
532
+ const { data, assessment, grade, learnerLocale } = input;
533
+ if (!aiSupports(data.type, "pronunciation-coaching") || !aiAllowedByContent(data, "pronunciation-coaching")) {
534
+ return null;
535
+ }
536
+ const item = data;
537
+ if (typeof item.referenceText !== "string" || typeof item.locale !== "string") {
538
+ return null;
539
+ }
540
+ const marks = assessment !== void 0 && assessment !== null ? marksThisText(item, assessment) ? fromAssessment(item, assessment) : null : grade !== void 0 && grade !== null ? fromGrade(grade) : null;
541
+ if (marks === null) {
542
+ return null;
543
+ }
544
+ const graded = grade !== void 0 && grade !== null && isFiniteNumber(grade.score) && isFiniteNumber(grade.maxScore) && typeof grade.passed === "boolean" ? { score: grade.score, maxScore: grade.maxScore, passed: grade.passed } : void 0;
545
+ return {
546
+ feature: "pronunciation-coaching",
547
+ facts: {
548
+ activityType: "read-aloud",
549
+ activityId: data.id,
550
+ title: data.title,
551
+ locale: item.locale,
552
+ referenceText: item.referenceText,
553
+ ...typeof item.instructions === "string" ? { instructions: item.instructions } : {},
554
+ ...marks
555
+ },
556
+ ...graded !== void 0 ? { grade: graded } : {},
557
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
558
+ };
559
+ }
560
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
561
+ var symbolKey = (symbol) => symbol.normalize("NFC").trim();
562
+ function checkAiCoaching(raw, request) {
563
+ const read = readAiTextResult(raw);
564
+ if (!read.ok) {
565
+ return read;
566
+ }
567
+ const entries = raw.words;
568
+ const words = [];
569
+ if (entries !== void 0 && entries !== null) {
570
+ if (!Array.isArray(entries)) {
571
+ return { ok: false, refusal: "malformed" };
572
+ }
573
+ if (entries.length > AI_COACHING_MAX_WORDS) {
574
+ return { ok: false, refusal: "too-long" };
575
+ }
576
+ const facts = /* @__PURE__ */ new Map();
577
+ request.facts.words.forEach((fact, order) => {
578
+ if (fact.itemId !== void 0) {
579
+ facts.set(fact.itemId, { fact, order });
580
+ }
581
+ });
582
+ const seen = /* @__PURE__ */ new Set();
583
+ for (const entry of entries) {
584
+ if (!isRecord2(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") {
585
+ return { ok: false, refusal: "malformed" };
586
+ }
587
+ const tip = cleanText(entry.tip);
588
+ if (tip === "") {
589
+ return { ok: false, refusal: "malformed" };
590
+ }
591
+ if ([...tip].length > AI_COACHING_MAX_TIP_LENGTH) {
592
+ return { ok: false, refusal: "too-long" };
593
+ }
594
+ if (seen.has(entry.itemId)) {
595
+ return { ok: false, refusal: "malformed" };
596
+ }
597
+ seen.add(entry.itemId);
598
+ const found = facts.get(entry.itemId);
599
+ if (found === void 0 || found.fact.state !== "mispronounced" && found.fact.state !== "omitted") {
600
+ return { ok: false, refusal: "contradicts-marks" };
601
+ }
602
+ const sound = checkSound(entry.sound, found.fact);
603
+ if (sound !== void 0 && "refusal" in sound) {
604
+ return sound;
605
+ }
606
+ words.push({
607
+ itemId: entry.itemId,
608
+ word: found.fact.word,
609
+ tip,
610
+ ...sound !== void 0 ? { sound } : {},
611
+ order: found.order
612
+ });
613
+ }
614
+ }
615
+ const { text: text2, provenance, usage } = read.result;
616
+ return {
617
+ ok: true,
618
+ coaching: {
619
+ text: text2,
620
+ words: words.sort((a, b) => a.order - b.order).map(({ order: _order, ...word }) => word),
621
+ ...provenance !== void 0 ? { provenance } : {},
622
+ ...usage !== void 0 ? { usage } : {}
623
+ }
624
+ };
625
+ }
626
+ function checkSound(raw, fact) {
627
+ if (raw === void 0 || raw === null) {
628
+ return void 0;
629
+ }
630
+ if (!isRecord2(raw) || typeof raw.expected !== "string" || raw.heard !== void 0 && raw.heard !== null && typeof raw.heard !== "string") {
631
+ return { ok: false, refusal: "malformed" };
632
+ }
633
+ const heard = typeof raw.heard === "string" ? raw.heard : void 0;
634
+ if ([...raw.expected].length > MAX_SYMBOL_LENGTH || heard !== void 0 && [...heard].length > MAX_SYMBOL_LENGTH) {
635
+ return { ok: false, refusal: "too-long" };
636
+ }
637
+ const expected = symbolKey(raw.expected);
638
+ const reported = _optionalChain([fact, 'access', _25 => _25.sounds, 'optionalAccess', _26 => _26.find, 'call', _27 => _27((sound) => symbolKey(sound.symbol) === expected)]);
639
+ if (reported === void 0) {
640
+ return { ok: false, refusal: "contradicts-marks" };
641
+ }
642
+ if (heard === void 0) {
643
+ return { expected: reported.symbol };
644
+ }
645
+ const instead = _optionalChain([reported, 'access', _28 => _28.heardAs, 'optionalAccess', _29 => _29.find, 'call', _30 => _30(
646
+ (candidate) => symbolKey(candidate.symbol) === symbolKey(heard)
647
+ )]);
648
+ if (instead === void 0) {
649
+ return { ok: false, refusal: "contradicts-marks" };
650
+ }
651
+ return { expected: reported.symbol, heard: instead.symbol };
652
+ }
653
+
654
+ // src/authoring/index.ts
655
+ function validateDraft(type, draft) {
656
+ return _chunk7THXD7ANcjs.withValidationScope.call(void 0, () => validateDraftInScope(type, draft));
657
+ }
658
+ function validateDraftInScope(type, draft) {
659
+ const descriptor = _chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, type);
660
+ if (descriptor === void 0) {
661
+ throw new (0, _chunk7THXD7ANcjs.UnknownActivityTypeError)(String(type));
662
+ }
663
+ const reported = _chunk7THXD7ANcjs.isRecord.call(void 0, draft) && _optionalChain([descriptor, 'access', _31 => _31.authoring, 'optionalAccess', _32 => _32.checkDraft]) !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
664
+ const issues = [...reported];
665
+ const parsed = _chunk7THXD7ANcjs.readSchema.call(void 0, descriptor.schema, draft, `Activity type "${descriptor.type}"`, {
666
+ reportInput: true
667
+ });
668
+ if (!parsed.success) {
669
+ const covered = _chunk7THXD7ANcjs.coveredPathsOf.call(void 0, reported);
670
+ const reportedEmpty = /* @__PURE__ */ new Set();
671
+ for (const schemaIssue of parsed.issues) {
672
+ const path = schemaIssue.path.map(String);
673
+ if (covered.has(_chunk7THXD7ANcjs.pathKey.call(void 0, path))) {
674
+ continue;
675
+ }
676
+ if (path.length > 0 && _chunk7THXD7ANcjs.refusesEmpty.call(void 0, draft, schemaIssue)) {
677
+ const key = JSON.stringify(path);
678
+ if (!reportedEmpty.has(key)) {
679
+ reportedEmpty.add(key);
680
+ issues.push(
681
+ _chunk7THXD7ANcjs.issue.call(void 0,
682
+ "null_not_allowed",
683
+ path,
684
+ typeof schemaIssue.path.at(-1) === "number" ? "This entry has no value. Remove it, or fill it in." : "This field cannot be null. Leave it out if it is optional, or give it a value."
685
+ )
686
+ );
687
+ }
688
+ continue;
689
+ }
690
+ issues.push({
691
+ path,
692
+ message: schemaIssue.message,
693
+ code: schemaIssue.code,
694
+ severity: "invalid"
695
+ });
696
+ }
697
+ }
698
+ if (parsed.success && issues.length === 0) {
699
+ return { status: "complete", data: parsed.data, issues };
700
+ }
701
+ const invalid = issues.some((found) => found.severity !== "incomplete");
702
+ return { status: invalid ? "invalid" : "incomplete", issues };
703
+ }
704
+ function createDraft(type, context) {
705
+ const descriptor = _chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, type);
706
+ if (descriptor === void 0) {
707
+ throw new (0, _chunk7THXD7ANcjs.UnknownActivityTypeError)(String(type));
708
+ }
709
+ const create = _optionalChain([descriptor, 'access', _33 => _33.authoring, 'optionalAccess', _34 => _34.createDraft]);
710
+ if (create === void 0) {
711
+ throw new Error(
712
+ `Activity type "${String(type)}" declares no authoring.createDraft, so there is no empty draft to create. Build the draft yourself; validateDraft works for every registered type.`
713
+ );
714
+ }
715
+ const issued = /* @__PURE__ */ new Set();
716
+ const newId = () => {
717
+ const id = context.newId();
718
+ if (typeof id !== "string" || id === "") {
719
+ throw new Error("createDraft: newId() must return a non-empty string.");
720
+ }
721
+ if (issued.has(id)) {
722
+ throw new Error(
723
+ `createDraft: newId() returned "${id}" twice. Ids within one draft must differ \u2014 two options sharing an id cannot be scored apart.`
724
+ );
725
+ }
726
+ issued.add(id);
727
+ return id;
728
+ };
729
+ return create({ newId });
730
+ }
731
+ function normalise(found) {
732
+ const documented = Object.hasOwn(_chunk7THXD7ANcjs.DRAFT_ISSUE_SEVERITY, found.code) ? _chunk7THXD7ANcjs.DRAFT_ISSUE_SEVERITY[found.code] : void 0;
733
+ return {
734
+ ...found,
735
+ path: found.path.map(String),
736
+ severity: _nullishCoalesce(documented, () => ( (found.severity === "incomplete" ? "incomplete" : "invalid")))
737
+ };
738
+ }
739
+ function critiqueDraft(type, draft) {
740
+ const descriptor = _chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, type);
741
+ if (descriptor === void 0) {
742
+ throw new (0, _chunk7THXD7ANcjs.UnknownActivityTypeError)(String(type));
743
+ }
744
+ const critique = _optionalChain([descriptor, 'access', _35 => _35.authoring, 'optionalAccess', _36 => _36.critique]);
745
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, draft) || critique === void 0) {
746
+ return [];
747
+ }
748
+ return _chunk7THXD7ANcjs.withValidationScope.call(void 0, () => critique(draft)).map(_chunk7THXD7ANcjs.normaliseFinding);
749
+ }
750
+
751
+ // src/authoring/item-group.ts
752
+ function createItemGroupDraft(context) {
753
+ const issued = /* @__PURE__ */ new Set();
754
+ const newId = () => {
755
+ const id = context.newId();
756
+ if (typeof id !== "string" || id === "") {
757
+ throw new Error("createItemGroupDraft: newId() must return a non-empty string.");
758
+ }
759
+ if (issued.has(id)) {
760
+ throw new Error(
761
+ `createItemGroupDraft: newId() returned "${id}" twice. Ids within one draft must differ.`
762
+ );
763
+ }
764
+ issued.add(id);
765
+ return id;
766
+ };
767
+ return {
768
+ schemaVersion: "1.0",
769
+ type: "item-group",
770
+ id: newId(),
771
+ stimulus: { id: newId(), kind: "text", body: "" },
772
+ items: []
773
+ };
774
+ }
775
+ function createInteractiveVideoDraft(context) {
776
+ const draft = createItemGroupDraft(context);
777
+ return {
778
+ ...draft,
779
+ stimulus: { id: draft.stimulus.id, kind: "video", media: { type: "video", url: "" } },
780
+ timeline: { cues: [] }
781
+ };
782
+ }
783
+ function critiqueItemGroupDraft(draft) {
784
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, draft)) {
785
+ return [];
786
+ }
787
+ const items = Array.isArray(draft.items) ? draft.items : [];
788
+ return _chunk7THXD7ANcjs.withValidationScope.call(void 0, () => [
789
+ ...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])),
790
+ ..._chunk7THXD7ANcjs.keyPositionFinding.call(void 0, items, ["items"])
791
+ ]);
792
+ }
793
+ function critiqueDrafts(entries) {
794
+ return _chunk7THXD7ANcjs.withValidationScope.call(void 0, () => [
795
+ ...entries.flatMap(
796
+ (entry, index) => repathed(
797
+ _chunk7THXD7ANcjs.isRecord.call(void 0, entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry),
798
+ [index]
799
+ )
800
+ ),
801
+ ..._chunk7THXD7ANcjs.keyPositionFinding.call(void 0, entries, [])
802
+ ]);
803
+ }
804
+ function critiqueItem(item) {
805
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, item) || typeof item.type !== "string") {
806
+ return [];
807
+ }
808
+ if (_chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, item.type) === void 0) {
809
+ return [];
810
+ }
811
+ return critiqueDraft(item.type, item);
812
+ }
813
+ var repathed = (findings, prefix) => findings.map((found) => ({ ...found, path: [...prefix.map(String), ...found.path] }));
814
+ function validateItemGroupDraft(draft) {
815
+ return _chunk7THXD7ANcjs.withValidationScope.call(void 0, () => validateItemGroupDraftInScope(draft));
816
+ }
817
+ function validateItemGroupDraftInScope(draft) {
818
+ const issues = [];
819
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, draft)) {
820
+ issues.push(
821
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_not_an_object", [], "A group must be an object with a stimulus and its items.")
822
+ );
823
+ return { status: "invalid", issues };
824
+ }
825
+ issues.push(...checkGroupIdentity(draft));
826
+ if (_chunk7THXD7ANcjs.isRecord.call(void 0, draft.stimulus)) {
827
+ issues.push(...checkStimulus(draft.stimulus));
828
+ } else if (_chunk7THXD7ANcjs.isUnset.call(void 0, draft.stimulus)) {
829
+ issues.push(
830
+ _chunk7THXD7ANcjs.issue.call(void 0,
831
+ "ig_stimulus_required",
832
+ ["stimulus"],
833
+ "Add the passage, recording or image the questions are about."
834
+ )
835
+ );
836
+ }
837
+ issues.push(...checkItems(draft));
838
+ if (draft.timeline !== void 0 && draft.timeline !== null) {
839
+ issues.push(...checkTimeline(draft));
840
+ }
841
+ const captions = _chunk7THXD7ANcjs.groupCaptionsField.call(void 0, draft, (captionsUrl) => !_chunk7THXD7ANcjs.isUnwritten.call(void 0, captionsUrl));
842
+ if (captions !== void 0) {
843
+ issues.push(
844
+ _chunk7THXD7ANcjs.issue.call(void 0,
845
+ "dc_captions_not_allowed",
846
+ ["stimulus", "media", captions],
847
+ _chunk7THXD7ANcjs.GROUP_CAPTIONS_REVEAL_DICTATION
848
+ )
849
+ );
850
+ }
851
+ const parsed = _chunkZWXQSKCXcjs.ItemGroupSchema.safeParse(draft, { reportInput: true });
852
+ if (!parsed.success) {
853
+ for (const schemaIssue of parsed.error.issues) {
854
+ const path = schemaIssue.path.map(String);
855
+ if (issues.some((known) => _chunk7THXD7ANcjs.covers.call(void 0, known.path, path))) {
856
+ continue;
857
+ }
858
+ if (path.length > 0 && _chunk7THXD7ANcjs.refusesEmpty.call(void 0, draft, schemaIssue)) {
859
+ issues.push(
860
+ _chunk7THXD7ANcjs.issue.call(void 0,
861
+ "null_not_allowed",
862
+ path,
863
+ "This field cannot be null. Leave it out if it is optional, or give it a value."
864
+ )
865
+ );
866
+ continue;
867
+ }
868
+ issues.push({
869
+ path,
870
+ message: schemaIssue.message,
871
+ code: schemaIssue.code,
872
+ severity: "invalid"
873
+ });
874
+ }
875
+ }
876
+ const stored = _chunkZWXQSKCXcjs.validateItemGroup.call(void 0, draft);
877
+ if (issues.length === 0 && stored.success) {
878
+ return { status: "complete", data: stored.data, issues };
879
+ }
880
+ if (issues.length === 0) {
881
+ for (const error of stored.success ? [] : stored.errors) {
882
+ issues.push({ ...error, severity: "invalid" });
883
+ }
884
+ }
885
+ const invalid = issues.some((found) => found.severity !== "incomplete");
886
+ return { status: invalid ? "invalid" : "incomplete", issues };
887
+ }
888
+ function checkTimeline(draft) {
889
+ const issues = [];
890
+ const timeline = draft.timeline;
891
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, timeline)) {
892
+ return issues;
893
+ }
894
+ const stimulus = _chunk7THXD7ANcjs.isRecord.call(void 0, draft.stimulus) ? draft.stimulus : void 0;
895
+ if (stimulus !== void 0 && !_chunk7THXD7ANcjs.isUnwritten.call(void 0, stimulus.kind) && stimulus.kind !== "video") {
896
+ issues.push(
897
+ _chunk7THXD7ANcjs.issue.call(void 0,
898
+ "ig_timeline_stimulus_kind",
899
+ ["stimulus", "kind"],
900
+ "An interactive video needs a video: quizzes open at moments of it."
901
+ )
902
+ );
903
+ } else if (stimulus !== void 0 && _chunk7THXD7ANcjs.isRecord.call(void 0, stimulus.media) && !_chunk7THXD7ANcjs.isUnwritten.call(void 0, stimulus.media.type) && stimulus.media.type !== "video") {
904
+ issues.push(
905
+ _chunk7THXD7ANcjs.issue.call(void 0,
906
+ "ig_timeline_stimulus_kind",
907
+ ["stimulus", "media", "type"],
908
+ "Upload a video file. An embedded player from another site cannot be paused when a quiz opens."
909
+ )
910
+ );
911
+ }
912
+ if (draft.shuffle === "within-group") {
913
+ issues.push(
914
+ _chunk7THXD7ANcjs.issue.call(void 0,
915
+ "ig_timeline_shuffle",
916
+ ["shuffle"],
917
+ "The questions of an interactive video appear in the order their quizzes open, so they cannot be shuffled."
918
+ )
919
+ );
920
+ }
921
+ const items = Array.isArray(draft.items) ? draft.items : [];
922
+ const itemIndexById = /* @__PURE__ */ new Map();
923
+ items.forEach((item, index) => {
924
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, item)) {
925
+ return;
926
+ }
927
+ if (typeof item.id === "string" && item.id !== "") {
928
+ itemIndexById.set(item.id, index);
929
+ }
930
+ const type = item.type;
931
+ if (typeof type === "string" && type !== "" && type !== "item-group" && !_chunkZWXQSKCXcjs.isInteractiveVideoItemType.call(void 0, type) && _chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, type) !== void 0) {
932
+ issues.push(
933
+ _chunk7THXD7ANcjs.issue.call(void 0,
934
+ "ig_timeline_item_type",
935
+ ["items", index, "type"],
936
+ `An interactive video can hold ${_chunkZWXQSKCXcjs.INTERACTIVE_VIDEO_ITEM_TYPES.join(", ")} questions only.`
937
+ )
938
+ );
939
+ }
940
+ if (type === "dictation" && _chunk7THXD7ANcjs.isUnset.call(void 0, item.media)) {
941
+ issues.push(
942
+ _chunk7THXD7ANcjs.issue.call(void 0,
943
+ "ig_timeline_dictation_media",
944
+ ["items", index, "media"],
945
+ "Add the recording this dictation plays: inside a video it cannot play the video."
946
+ )
947
+ );
948
+ }
949
+ });
950
+ const cues = Array.isArray(timeline.cues) ? timeline.cues : [];
951
+ const quizIds = /* @__PURE__ */ new Set();
952
+ const placed = /* @__PURE__ */ new Set();
953
+ cues.forEach((cue, cueIndex) => {
954
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, cue)) {
955
+ return;
956
+ }
957
+ const at = (field, ...rest) => [
958
+ "timeline",
959
+ "cues",
960
+ cueIndex,
961
+ field,
962
+ ...rest
963
+ ];
964
+ if (_chunk7THXD7ANcjs.isMissingId.call(void 0, cue.id)) {
965
+ issues.push(_chunk7THXD7ANcjs.issue.call(void 0, "ig_timeline_quiz_id_required", at("id"), "This quiz has no id."));
966
+ } else if (typeof cue.id === "string") {
967
+ if (quizIds.has(cue.id)) {
968
+ issues.push(
969
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_timeline_quiz_id_duplicate", at("id"), `Two quizzes share the id "${cue.id}".`)
970
+ );
971
+ }
972
+ quizIds.add(cue.id);
973
+ }
974
+ if (_chunk7THXD7ANcjs.isUnset.call(void 0, cue.at)) {
975
+ issues.push(
976
+ _chunk7THXD7ANcjs.issue.call(void 0,
977
+ "ig_timeline_quiz_time_required",
978
+ at("at"),
979
+ "Choose when in the video this quiz opens."
980
+ )
981
+ );
982
+ } else if (typeof cue.at !== "number" || !Number.isFinite(cue.at) || cue.at < 0) {
983
+ issues.push(
984
+ _chunk7THXD7ANcjs.issue.call(void 0,
985
+ "ig_timeline_quiz_time_invalid",
986
+ at("at"),
987
+ "A quiz opens at a number of seconds, 0 or more."
988
+ )
989
+ );
990
+ }
991
+ if (_chunk7THXD7ANcjs.isUnset.call(void 0, cue.itemIds) || Array.isArray(cue.itemIds) && cue.itemIds.length === 0) {
992
+ issues.push(
993
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_timeline_quiz_empty", at("itemIds"), "Add at least one question to this quiz.")
994
+ );
995
+ return;
996
+ }
997
+ if (!Array.isArray(cue.itemIds)) {
998
+ return;
999
+ }
1000
+ cue.itemIds.forEach((itemId, position) => {
1001
+ if (typeof itemId !== "string") {
1002
+ return;
1003
+ }
1004
+ if (!itemIndexById.has(itemId)) {
1005
+ issues.push(
1006
+ _chunk7THXD7ANcjs.issue.call(void 0,
1007
+ "ig_timeline_quiz_unknown_item",
1008
+ at("itemIds", position),
1009
+ `This quiz names a question that is not in the video ("${itemId}").`
1010
+ )
1011
+ );
1012
+ } else if (placed.has(itemId)) {
1013
+ issues.push(
1014
+ _chunk7THXD7ANcjs.issue.call(void 0,
1015
+ "ig_timeline_item_duplicate",
1016
+ at("itemIds", position),
1017
+ "This question is already in a quiz: each question belongs to one quiz."
1018
+ )
1019
+ );
1020
+ }
1021
+ placed.add(itemId);
1022
+ });
1023
+ });
1024
+ for (const [itemId, index] of itemIndexById) {
1025
+ if (!placed.has(itemId)) {
1026
+ issues.push(
1027
+ _chunk7THXD7ANcjs.issue.call(void 0,
1028
+ "ig_timeline_item_unplaced",
1029
+ ["items", index],
1030
+ "Add this question to a quiz, or the video never shows it."
1031
+ )
1032
+ );
1033
+ }
1034
+ }
1035
+ const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
1036
+ let previousAt;
1037
+ chapters.forEach((chapter, index) => {
1038
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, chapter)) {
1039
+ return;
1040
+ }
1041
+ if (_chunk7THXD7ANcjs.isUnwritten.call(void 0, chapter.title)) {
1042
+ issues.push(
1043
+ _chunk7THXD7ANcjs.issue.call(void 0,
1044
+ "ig_timeline_chapter_title",
1045
+ ["timeline", "chapters", index, "title"],
1046
+ "Give this chapter a title."
1047
+ )
1048
+ );
1049
+ }
1050
+ if (typeof chapter.at === "number" && Number.isFinite(chapter.at)) {
1051
+ if (previousAt !== void 0 && chapter.at <= previousAt) {
1052
+ issues.push(
1053
+ _chunk7THXD7ANcjs.issue.call(void 0,
1054
+ "ig_timeline_chapter_order",
1055
+ ["timeline", "chapters", index, "at"],
1056
+ "Chapters must start in order, each after the one before it."
1057
+ )
1058
+ );
1059
+ }
1060
+ previousAt = chapter.at;
1061
+ }
1062
+ });
1063
+ return issues;
1064
+ }
1065
+ function checkGroupIdentity(draft) {
1066
+ const issues = [];
1067
+ if (draft.schemaVersion !== "1.0") {
1068
+ issues.push(
1069
+ _chunk7THXD7ANcjs.issue.call(void 0,
1070
+ "schema_version_invalid",
1071
+ ["schemaVersion"],
1072
+ 'The draft must have schemaVersion "1.0". A draft from createItemGroupDraft has it.'
1073
+ )
1074
+ );
1075
+ }
1076
+ if (draft.type !== "item-group") {
1077
+ issues.push(_chunk7THXD7ANcjs.issue.call(void 0, "type_mismatch", ["type"], `The draft's type must be "item-group".`));
1078
+ }
1079
+ if (_chunk7THXD7ANcjs.isMissingId.call(void 0, draft.id)) {
1080
+ issues.push(_chunk7THXD7ANcjs.issue.call(void 0, "id_required", ["id"], "The group has no id."));
1081
+ }
1082
+ if (!_chunk7THXD7ANcjs.isUnset.call(void 0, draft.shuffle) && draft.shuffle !== "none" && draft.shuffle !== "within-group") {
1083
+ issues.push(
1084
+ _chunk7THXD7ANcjs.issue.call(void 0,
1085
+ "ig_shuffle_invalid",
1086
+ ["shuffle"],
1087
+ 'Shuffling is either "none" or "within-group". Leave it out to keep the authored order.'
1088
+ )
1089
+ );
1090
+ }
1091
+ return issues;
1092
+ }
1093
+ var KINDS = /* @__PURE__ */ new Set(["text", "audio", "video", "image", "mixed"]);
1094
+ var MEDIA_FOR_KIND = {
1095
+ audio: ["audio"],
1096
+ video: ["video", "embed"],
1097
+ image: ["image"],
1098
+ mixed: ["audio", "video", "image", "embed"]
1099
+ };
1100
+ function checkStimulus(stimulus) {
1101
+ const issues = [];
1102
+ const at = (...rest) => ["stimulus", ...rest];
1103
+ if (_chunk7THXD7ANcjs.isMissingId.call(void 0, stimulus.id)) {
1104
+ issues.push(_chunk7THXD7ANcjs.issue.call(void 0, "ig_stimulus_id_required", at("id"), "The stimulus has no id."));
1105
+ }
1106
+ const kind = stimulus.kind;
1107
+ if (_chunk7THXD7ANcjs.isUnwritten.call(void 0, kind)) {
1108
+ issues.push(
1109
+ _chunk7THXD7ANcjs.issue.call(void 0,
1110
+ "ig_stimulus_kind_required",
1111
+ at("kind"),
1112
+ "Choose what the questions are about: a text, a recording, a video, an image, or a mix."
1113
+ )
1114
+ );
1115
+ } else if (typeof kind !== "string" || !KINDS.has(kind)) {
1116
+ if (typeof kind === "string") {
1117
+ issues.push(
1118
+ _chunk7THXD7ANcjs.issue.call(void 0,
1119
+ "ig_stimulus_kind_invalid",
1120
+ at("kind"),
1121
+ `"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`
1122
+ )
1123
+ );
1124
+ }
1125
+ if (_chunk7THXD7ANcjs.isRecord.call(void 0, stimulus.media)) {
1126
+ issues.push(..._chunk7THXD7ANcjs.checkMedia.call(void 0, stimulus.media, at("media")));
1127
+ }
1128
+ return issues;
1129
+ }
1130
+ const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
1131
+ const htmlPresent = !_chunk7THXD7ANcjs.isUnset.call(void 0, stimulus.bodyHtml);
1132
+ if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) {
1133
+ issues.push(
1134
+ _chunk7THXD7ANcjs.issue.call(void 0,
1135
+ "ig_stimulus_body_required",
1136
+ at("body"),
1137
+ _chunk7THXD7ANcjs.isUnwritten.call(void 0, stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."
1138
+ )
1139
+ );
1140
+ }
1141
+ if (kind !== "text" && _chunk7THXD7ANcjs.isUnset.call(void 0, stimulus.media)) {
1142
+ issues.push(
1143
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_stimulus_media_required", at("media"), "Add the recording, video or image.")
1144
+ );
1145
+ } else if (_chunk7THXD7ANcjs.isRecord.call(void 0, stimulus.media)) {
1146
+ const allowed = MEDIA_FOR_KIND[kind];
1147
+ const mediaType = stimulus.media.type;
1148
+ if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) {
1149
+ issues.push(
1150
+ _chunk7THXD7ANcjs.issue.call(void 0,
1151
+ "ig_stimulus_media_kind",
1152
+ at("media", "type"),
1153
+ `A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`
1154
+ )
1155
+ );
1156
+ }
1157
+ issues.push(..._chunk7THXD7ANcjs.checkMedia.call(void 0, stimulus.media, at("media")));
1158
+ }
1159
+ return issues;
1160
+ }
1161
+ function checkItems(draft) {
1162
+ const issues = [];
1163
+ const items = _chunk7THXD7ANcjs.isUnset.call(void 0, draft.items) ? [] : draft.items;
1164
+ if (!Array.isArray(items)) {
1165
+ return issues;
1166
+ }
1167
+ if (items.length === 0) {
1168
+ issues.push(
1169
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_items_required", ["items"], "Add at least one question about this material.")
1170
+ );
1171
+ return issues;
1172
+ }
1173
+ const seen = /* @__PURE__ */ new Set();
1174
+ const repeated = /* @__PURE__ */ new Set();
1175
+ for (const [index, item] of items.entries()) {
1176
+ if (!_chunk7THXD7ANcjs.isRecord.call(void 0, item)) {
1177
+ continue;
1178
+ }
1179
+ const ordinal = index + 1;
1180
+ if (_chunk7THXD7ANcjs.isMissingId.call(void 0, item.id)) {
1181
+ issues.push(
1182
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_item_id_required", ["items", index, "id"], `Question ${ordinal} has no id.`)
1183
+ );
1184
+ } else if (typeof item.id === "string") {
1185
+ if (seen.has(item.id)) {
1186
+ repeated.add(item.id);
1187
+ }
1188
+ seen.add(item.id);
1189
+ }
1190
+ if (item.type === "item-group") {
1191
+ issues.push(
1192
+ _chunk7THXD7ANcjs.issue.call(void 0,
1193
+ "ig_item_nested_group",
1194
+ ["items", index, "type"],
1195
+ "Groups do not nest: every item must be an activity."
1196
+ )
1197
+ );
1198
+ continue;
1199
+ }
1200
+ if (_chunk7THXD7ANcjs.isUnwritten.call(void 0, item.type)) {
1201
+ issues.push(
1202
+ _chunk7THXD7ANcjs.issue.call(void 0,
1203
+ "ig_item_type_required",
1204
+ ["items", index, "type"],
1205
+ `Choose what kind of question ${ordinal} is.`
1206
+ )
1207
+ );
1208
+ continue;
1209
+ }
1210
+ if (typeof item.type !== "string") {
1211
+ continue;
1212
+ }
1213
+ let result;
1214
+ try {
1215
+ result = validateDraft(item.type, item);
1216
+ } catch (error) {
1217
+ if (!(error instanceof _chunk7THXD7ANcjs.UnknownActivityTypeError) || error.activityType !== item.type) {
1218
+ throw error;
1219
+ }
1220
+ issues.push(
1221
+ _chunk7THXD7ANcjs.issue.call(void 0,
1222
+ "ig_item_type_unknown",
1223
+ ["items", index, "type"],
1224
+ `"${item.type}" is not a registered activity type.`
1225
+ )
1226
+ );
1227
+ continue;
1228
+ }
1229
+ for (const found of result.issues) {
1230
+ issues.push({ ...found, path: ["items", String(index), ...found.path] });
1231
+ }
1232
+ }
1233
+ for (const id of repeated) {
1234
+ issues.push(
1235
+ _chunk7THXD7ANcjs.issue.call(void 0, "ig_item_id_duplicate", ["items"], `More than one question has the id "${id}".`)
1236
+ );
1237
+ }
1238
+ return issues;
1239
+ }
1240
+
1241
+ // src/ai-critique.ts
1242
+ var AI_CRITIQUE_MAX_FINDINGS = 20;
1243
+ var AI_CRITIQUE_MAX_FIELD_LENGTH = 500;
1244
+ var KINDS2 = [
1245
+ "ambiguous",
1246
+ "second-answer",
1247
+ "wrong-key",
1248
+ "implausible-distractor",
1249
+ "cue",
1250
+ "level",
1251
+ "language",
1252
+ "sensitivity",
1253
+ "other"
1254
+ ];
1255
+ var NOT_WORDS = /* @__PURE__ */ new Set([
1256
+ "id",
1257
+ "type",
1258
+ "schemaVersion",
1259
+ "mode",
1260
+ "scoringStrategy",
1261
+ "presentation",
1262
+ "locale",
1263
+ "languageTarget",
1264
+ "correctChoiceId",
1265
+ "bankId",
1266
+ "url",
1267
+ "captionsUrl",
1268
+ "match",
1269
+ "playback",
1270
+ "recording",
1271
+ "scoring",
1272
+ "ai",
1273
+ "learningObjectives",
1274
+ "hints",
1275
+ "kind",
1276
+ "timeline"
1277
+ ]);
1278
+ function fieldsOf(value, path = []) {
1279
+ if (typeof value === "string") {
1280
+ return value.trim() === "" ? [] : [{ path, text: value }];
1281
+ }
1282
+ if (Array.isArray(value)) {
1283
+ return value.flatMap((entry, index) => fieldsOf(entry, [...path, String(index)]));
1284
+ }
1285
+ if (typeof value === "object" && value !== null) {
1286
+ return Object.entries(value).flatMap(
1287
+ ([key, entry]) => NOT_WORDS.has(key) || key.endsWith("Html") ? [] : fieldsOf(entry, [...path, key])
1288
+ );
1289
+ }
1290
+ return [];
1291
+ }
1292
+ function aiCritiqueRequest(input) {
1293
+ const { type, draft, level, authorLocale } = input;
1294
+ if (_chunk7THXD7ANcjs.getActivityTypeDescriptor.call(void 0, type) === void 0) {
1295
+ throw new (0, _chunk7THXD7ANcjs.UnknownActivityTypeError)(String(type));
1296
+ }
1297
+ if (typeof draft !== "object" || draft === null || Array.isArray(draft)) {
1298
+ return null;
1299
+ }
1300
+ const fields = fieldsOf(draft);
1301
+ if (fields.length === 0) {
1302
+ return null;
1303
+ }
1304
+ return {
1305
+ feature: "item-critique",
1306
+ facts: {
1307
+ activityType: String(type),
1308
+ item: draft,
1309
+ fields,
1310
+ findings: critiqueDraft(type, draft).map(({ path, code, message }) => ({
1311
+ path,
1312
+ code,
1313
+ message
1314
+ })),
1315
+ ...level !== void 0 ? { level } : {}
1316
+ },
1317
+ ...authorLocale !== void 0 ? { authorLocale } : {}
1318
+ };
1319
+ }
1320
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1321
+ var tooLong = (text2) => [...text2].length > AI_CRITIQUE_MAX_FIELD_LENGTH;
1322
+ function checkAiCritique(raw, request) {
1323
+ if (!isRecord3(raw) || !Array.isArray(raw.findings)) {
1324
+ return { ok: false, refusal: "malformed" };
1325
+ }
1326
+ if (raw.findings.length > AI_CRITIQUE_MAX_FINDINGS) {
1327
+ return { ok: false, refusal: "too-long" };
1328
+ }
1329
+ const fields = new Map(request.facts.fields.map((field) => [JSON.stringify(field.path), field]));
1330
+ const findings = [];
1331
+ for (const entry of raw.findings) {
1332
+ if (!isRecord3(entry) || !Array.isArray(entry.path) || !entry.path.every((segment) => typeof segment === "string" || typeof segment === "number") || typeof entry.kind !== "string" || !KINDS2.includes(entry.kind) || typeof entry.message !== "string" || entry.quote !== void 0 && entry.quote !== null && typeof entry.quote !== "string") {
1333
+ return { ok: false, refusal: "malformed" };
1334
+ }
1335
+ const message = cleanText(entry.message);
1336
+ const quote = typeof entry.quote === "string" ? cleanText(entry.quote) : void 0;
1337
+ if (message === "" || quote === "") {
1338
+ return { ok: false, refusal: "malformed" };
1339
+ }
1340
+ if (tooLong(message) || quote !== void 0 && tooLong(quote)) {
1341
+ return { ok: false, refusal: "too-long" };
1342
+ }
1343
+ const field = fields.get(JSON.stringify(entry.path.map(String)));
1344
+ if (field === void 0) {
1345
+ return { ok: false, refusal: "contradicts-item" };
1346
+ }
1347
+ if (quote !== void 0 && !folded(field.text).chars.includes(needle(quote))) {
1348
+ return { ok: false, refusal: "contradicts-item" };
1349
+ }
1350
+ findings.push({
1351
+ path: field.path,
1352
+ code: `ai_${entry.kind.replace(/-/g, "_")}`,
1353
+ message,
1354
+ severity: "advice",
1355
+ ...quote !== void 0 ? { quote } : {}
1356
+ });
1357
+ }
1358
+ const provenance = provenanceOf(raw.provenance);
1359
+ const usage = usageOf(raw.usage);
1360
+ return {
1361
+ ok: true,
1362
+ critique: {
1363
+ findings,
1364
+ ...provenance !== void 0 ? { provenance } : {},
1365
+ ...usage !== void 0 ? { usage } : {}
1366
+ }
1367
+ };
1368
+ }
1369
+
1370
+ // src/ai-drafts.ts
1371
+ var AI_DRAFTS_MAX_COUNT = 50;
1372
+ var AI_DRAFTS_MAX_REPAIRS = 3;
1373
+ var DRAFT_TYPES = [
1374
+ "multiple-choice",
1375
+ "fill-in-the-blanks",
1376
+ "gap-select",
1377
+ "dictation",
1378
+ "read-aloud",
1379
+ "written-response"
1380
+ ];
1381
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1382
+ var text = (description) => ({ type: "string", description });
1383
+ var FEEDBACK = {
1384
+ type: "object",
1385
+ description: "Optional overall feedback, shown after answering.",
1386
+ properties: {
1387
+ correct: text("Shown when the learner passes."),
1388
+ incorrect: text("Shown when the learner does not.")
1389
+ }
1390
+ };
1391
+ var FIELDS = {
1392
+ title: text(
1393
+ "A short name for the item, shown to the learner above it. Never the answer, or any part of it."
1394
+ ),
1395
+ question: text("The question, as plain text."),
1396
+ mode: {
1397
+ type: "string",
1398
+ enum: ["single", "multi"],
1399
+ description: "`single`: exactly one option is right. `multi`: one or more are."
1400
+ },
1401
+ options: {
1402
+ type: "array",
1403
+ minItems: 2,
1404
+ maxItems: 26,
1405
+ items: {
1406
+ type: "object",
1407
+ required: ["text", "isCorrect"],
1408
+ properties: {
1409
+ text: text("The option as the learner reads it."),
1410
+ isCorrect: { type: "boolean" },
1411
+ feedback: text("Why this option is right or wrong, shown after answering.")
1412
+ }
1413
+ }
1414
+ },
1415
+ passage: text(
1416
+ "The passage as plain text, with {{1}}, {{2}}, \u2026 where each gap goes, numbered in the order they appear."
1417
+ ),
1418
+ blanks: {
1419
+ type: "array",
1420
+ minItems: 1,
1421
+ description: "One per gap, in the order the gaps appear: the first is {{1}}.",
1422
+ items: {
1423
+ type: "object",
1424
+ required: ["acceptedAnswers"],
1425
+ properties: {
1426
+ acceptedAnswers: {
1427
+ type: "array",
1428
+ minItems: 1,
1429
+ items: { type: "string" },
1430
+ description: "Every answer that is right, spelled as a learner would type it."
1431
+ },
1432
+ hint: text("Optional. Helps without giving the answer or any of its words.")
1433
+ }
1434
+ }
1435
+ },
1436
+ gaps: {
1437
+ type: "array",
1438
+ minItems: 1,
1439
+ description: "One per gap, in the order the gaps appear: the first is {{1}}.",
1440
+ items: {
1441
+ type: "object",
1442
+ required: ["choices"],
1443
+ properties: {
1444
+ choices: {
1445
+ type: "array",
1446
+ minItems: 2,
1447
+ description: "What the learner chooses from. Exactly one is right.",
1448
+ items: {
1449
+ type: "object",
1450
+ required: ["text", "isCorrect"],
1451
+ properties: { text: text("A choice."), isCorrect: { type: "boolean" } }
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+ },
1457
+ transcript: text(
1458
+ "What the learner hears and types: words from the source, as they are said there."
1459
+ ),
1460
+ referenceText: text("What the learner reads aloud: a sentence or two from the source."),
1461
+ instructions: text("Optional. How to read it, shown to the learner."),
1462
+ prompt: text("The writing task, as plain text."),
1463
+ minWords: { type: "integer", minimum: 0 },
1464
+ maxWords: { type: "integer", minimum: 1 },
1465
+ rubric: {
1466
+ type: "object",
1467
+ description: "Optional. What the writing is assessed on; every criterion is weighted equally.",
1468
+ properties: {
1469
+ criteria: {
1470
+ type: "array",
1471
+ minItems: 1,
1472
+ items: {
1473
+ type: "object",
1474
+ required: ["name"],
1475
+ properties: { name: text("A short name."), description: text("What it looks for.") }
1476
+ }
1477
+ }
1478
+ }
1479
+ },
1480
+ feedback: FEEDBACK
1481
+ };
1482
+ var WRITES = {
1483
+ "multiple-choice": ["title", "question", "mode", "options", "feedback"],
1484
+ "fill-in-the-blanks": ["title", "passage", "blanks", "feedback"],
1485
+ "gap-select": ["title", "passage", "gaps", "feedback"],
1486
+ dictation: ["title", "transcript", "feedback"],
1487
+ "read-aloud": ["title", "referenceText", "instructions", "feedback"],
1488
+ "written-response": ["title", "prompt", "minWords", "maxWords", "rubric"]
1489
+ };
1490
+ function itemShape(types, captions) {
1491
+ const properties = {
1492
+ type: {
1493
+ type: "string",
1494
+ enum: [...types],
1495
+ description: `The item's type, and the fields it fills. ${types.map((type) => `${type}: ${WRITES[type].filter((name) => name !== "title").join(", ")}`).join(". ")}.`
1496
+ }
1497
+ };
1498
+ for (const type of types) {
1499
+ for (const name of WRITES[type]) {
1500
+ properties[name] = FIELDS[name];
1501
+ }
1502
+ }
1503
+ if (captions) {
1504
+ properties.caption = {
1505
+ type: "integer",
1506
+ minimum: 0,
1507
+ description: "The index of the caption the item is about. It opens when that caption ends."
1508
+ };
1509
+ }
1510
+ return {
1511
+ type: "object",
1512
+ required: captions ? ["type", "title", "caption"] : ["type", "title"],
1513
+ properties
1514
+ };
1515
+ }
1516
+ function replyShape(types, captions, max, exactly) {
1517
+ return {
1518
+ type: "object",
1519
+ required: ["drafts"],
1520
+ properties: {
1521
+ drafts: {
1522
+ type: "array",
1523
+ ...exactly !== void 0 ? { minItems: exactly, maxItems: exactly } : { maxItems: max },
1524
+ items: itemShape(types, captions)
1525
+ }
1526
+ }
1527
+ };
1528
+ }
1529
+ function aiDraftsRequest(input) {
1530
+ const { types, source, count, locale, level, instructions, settings } = input;
1531
+ if (!Array.isArray(types) || types.length === 0 || new Set(types).size !== types.length || !types.every((type) => DRAFT_TYPES.includes(type))) {
1532
+ throw new RangeError(
1533
+ `aiDraftsRequest: types must be one or more of ${DRAFT_TYPES.join(", ")}, each once, in the author's order.`
1534
+ );
1535
+ }
1536
+ if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > AI_DRAFTS_MAX_COUNT)) {
1537
+ throw new RangeError(
1538
+ `aiDraftsRequest: count must be a whole number from 1 to ${AI_DRAFTS_MAX_COUNT}, or left out, not ${String(count)}.`
1539
+ );
1540
+ }
1541
+ let facts;
1542
+ if (source.kind === "captions") {
1543
+ const captions = (Array.isArray(source.cues) ? source.cues : []).flatMap(
1544
+ (cue, index) => isRecord4(cue) && typeof cue.text === "string" && cue.text.trim() !== "" && Number.isFinite(cue.start) && Number.isFinite(cue.end) ? [{ index, start: cue.start, end: cue.end, text: cue.text }] : []
1545
+ );
1546
+ if (captions.length === 0) {
1547
+ return null;
1548
+ }
1549
+ facts = { kind: "captions", captions };
1550
+ } else {
1551
+ if (typeof source.text !== "string" || source.text.trim() === "") {
1552
+ return null;
1553
+ }
1554
+ facts = { kind: source.kind, text: source.text };
1555
+ }
1556
+ return {
1557
+ feature: "draft-generation",
1558
+ facts: {
1559
+ activityTypes: [...types],
1560
+ source: facts,
1561
+ ...count !== void 0 ? { count } : {},
1562
+ ...locale !== void 0 ? { locale } : {},
1563
+ ...level !== void 0 ? { level } : {},
1564
+ ...instructions !== void 0 ? { instructions } : {}
1565
+ },
1566
+ shape: replyShape(types, facts.kind === "captions", _nullishCoalesce(count, () => ( AI_DRAFTS_MAX_COUNT))),
1567
+ ...settings !== void 0 ? { settings } : {}
1568
+ };
1569
+ }
1570
+ var stringOf = (value) => typeof value === "string" ? value : "";
1571
+ var listOf2 = (value) => Array.isArray(value) ? value : [];
1572
+ var written = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
1573
+ function feedbackOf(entry) {
1574
+ if (!isRecord4(entry.feedback)) {
1575
+ return {};
1576
+ }
1577
+ const correct = written(entry.feedback.correct);
1578
+ const incorrect = written(entry.feedback.incorrect);
1579
+ return correct === void 0 && incorrect === void 0 ? {} : {
1580
+ feedback: {
1581
+ ...correct !== void 0 ? { correct } : {},
1582
+ ...incorrect !== void 0 ? { incorrect } : {}
1583
+ }
1584
+ };
1585
+ }
1586
+ function contentOf(type, entry, newId) {
1587
+ switch (type) {
1588
+ case "multiple-choice":
1589
+ return {
1590
+ question: stringOf(entry.question),
1591
+ ...typeof entry.mode === "string" ? { mode: entry.mode } : {},
1592
+ options: listOf2(entry.options).map((option) => {
1593
+ const fields = isRecord4(option) ? option : {};
1594
+ const feedback = written(fields.feedback);
1595
+ return {
1596
+ id: newId(),
1597
+ text: stringOf(fields.text),
1598
+ isCorrect: fields.isCorrect === true,
1599
+ ...feedback !== void 0 ? { feedback } : {}
1600
+ };
1601
+ }),
1602
+ ...feedbackOf(entry)
1603
+ };
1604
+ case "fill-in-the-blanks":
1605
+ return {
1606
+ passage: stringOf(entry.passage),
1607
+ blanks: listOf2(entry.blanks).map((blank, index) => {
1608
+ const fields = isRecord4(blank) ? blank : {};
1609
+ const hint = written(fields.hint);
1610
+ return {
1611
+ id: String(index + 1),
1612
+ acceptedAnswers: listOf2(fields.acceptedAnswers).filter(
1613
+ (answer) => typeof answer === "string"
1614
+ ),
1615
+ ...hint !== void 0 ? { hint } : {}
1616
+ };
1617
+ }),
1618
+ ...feedbackOf(entry)
1619
+ };
1620
+ case "gap-select":
1621
+ return {
1622
+ passage: stringOf(entry.passage),
1623
+ gaps: listOf2(entry.gaps).map((gap, index) => {
1624
+ const marked = listOf2(isRecord4(gap) ? gap.choices : void 0);
1625
+ const choices = marked.map((choice) => ({
1626
+ id: newId(),
1627
+ text: stringOf(isRecord4(choice) ? choice.text : void 0)
1628
+ }));
1629
+ const right = marked.flatMap(
1630
+ (choice, at) => isRecord4(choice) && choice.isCorrect === true ? [at] : []
1631
+ );
1632
+ return {
1633
+ id: String(index + 1),
1634
+ choices,
1635
+ correctChoiceId: right.length === 1 ? _optionalChain([choices, 'access', _37 => _37[right[0]], 'optionalAccess', _38 => _38.id]) : ""
1636
+ };
1637
+ }),
1638
+ ...feedbackOf(entry)
1639
+ };
1640
+ case "dictation":
1641
+ return { transcript: stringOf(entry.transcript), ...feedbackOf(entry) };
1642
+ case "read-aloud": {
1643
+ const instructions = written(entry.instructions);
1644
+ return {
1645
+ referenceText: stringOf(entry.referenceText),
1646
+ ...instructions !== void 0 ? { instructions } : {},
1647
+ ...feedbackOf(entry)
1648
+ };
1649
+ }
1650
+ default: {
1651
+ const criteria = listOf2(isRecord4(entry.rubric) ? entry.rubric.criteria : void 0);
1652
+ return {
1653
+ prompt: stringOf(entry.prompt),
1654
+ ...entry.minWords !== void 0 ? { minWords: entry.minWords } : {},
1655
+ ...entry.maxWords !== void 0 ? { maxWords: entry.maxWords } : {},
1656
+ ...criteria.length > 0 ? {
1657
+ rubric: {
1658
+ criteria: criteria.map((criterion) => {
1659
+ const fields = isRecord4(criterion) ? criterion : {};
1660
+ const description = written(fields.description);
1661
+ return {
1662
+ name: stringOf(fields.name),
1663
+ ...description !== void 0 ? { description } : {},
1664
+ weight: 1
1665
+ };
1666
+ })
1667
+ }
1668
+ } : {}
1669
+ };
1670
+ }
1671
+ }
1672
+ }
1673
+ function startOf(type) {
1674
+ switch (type) {
1675
+ case "multiple-choice":
1676
+ case "fill-in-the-blanks":
1677
+ case "gap-select":
1678
+ return { scoringStrategy: "all-or-nothing" };
1679
+ case "read-aloud":
1680
+ return { recording: { maxSeconds: 0 }, scoring: { dimensions: [] } };
1681
+ default:
1682
+ return {};
1683
+ }
1684
+ }
1685
+ function draftFrom(type, entry, id, newId, request) {
1686
+ const locale = request.facts.locale;
1687
+ return {
1688
+ ...startOf(type),
1689
+ ...locale !== void 0 ? { locale } : {},
1690
+ ..._optionalChain([request, 'access', _39 => _39.settings, 'optionalAccess', _40 => _40[type]]),
1691
+ title: stringOf(entry.title),
1692
+ ...contentOf(type, entry, newId),
1693
+ schemaVersion: "1.0",
1694
+ type,
1695
+ id
1696
+ };
1697
+ }
1698
+ function idSource(newId, caller) {
1699
+ const issued = /* @__PURE__ */ new Set();
1700
+ return () => {
1701
+ const id = newId();
1702
+ if (typeof id !== "string" || id === "") {
1703
+ throw new Error(`${caller}: newId() must return a non-empty string.`);
1704
+ }
1705
+ if (issued.has(id)) {
1706
+ throw new Error(
1707
+ `${caller}: newId() returned "${id}" twice. Ids must differ \u2014 two options sharing an id cannot be scored apart.`
1708
+ );
1709
+ }
1710
+ issued.add(id);
1711
+ return id;
1712
+ };
1713
+ }
1714
+ function inAuthorsOrder(drafts, types) {
1715
+ return [...drafts].sort(
1716
+ (a, b) => types.indexOf(a.type) - types.indexOf(b.type) || a.index - b.index
1717
+ );
1718
+ }
1719
+ function check(raw, request, newId, keep = []) {
1720
+ const types = request.facts.activityTypes;
1721
+ if (!isRecord4(raw) || !Array.isArray(raw.drafts) || !raw.drafts.every((entry) => isRecord4(entry) && types.includes(entry.type))) {
1722
+ return { ok: false, refusal: "malformed" };
1723
+ }
1724
+ const entries = raw.drafts;
1725
+ const repair = request.repair;
1726
+ const most = _nullishCoalesce(request.facts.count, () => ( AI_DRAFTS_MAX_COUNT));
1727
+ if (repair !== void 0 ? entries.length !== repair.length : entries.length > most) {
1728
+ return { ok: false, refusal: repair !== void 0 ? "malformed" : "too-long" };
1729
+ }
1730
+ const source = request.facts.source;
1731
+ const drafts = entries.map((entry, position) => {
1732
+ const type = entry.type;
1733
+ const draft = draftFrom(type, entry, _nullishCoalesce(keep[position], () => ( newId())), newId, request);
1734
+ const caption = source.kind === "captions" && Number.isInteger(entry.caption) ? source.captions.find((one) => one.index === entry.caption) : void 0;
1735
+ const said = _chunk7THXD7ANcjs.fold.call(void 0, stringOf(entry.transcript));
1736
+ const clip = type === "dictation" && caption !== void 0 && _chunk7THXD7ANcjs.containsWords.call(void 0, _chunk7THXD7ANcjs.fold.call(void 0, caption.text), said) ? { start: caption.start, end: caption.end } : void 0;
1737
+ return {
1738
+ index: repair !== void 0 ? _optionalChain([repair, 'access', _41 => _41[position], 'optionalAccess', _42 => _42.index]) : position,
1739
+ type,
1740
+ draft,
1741
+ validation: validateDraft(type, draft),
1742
+ findings: critiqueDraft(type, draft),
1743
+ ...caption !== void 0 ? { at: caption.end } : {},
1744
+ ...clip !== void 0 ? { clip } : {},
1745
+ generated: entry
1746
+ };
1747
+ });
1748
+ const ordered = inAuthorsOrder(drafts, types);
1749
+ const provenance = provenanceOf(raw.provenance);
1750
+ const usage = usageOf(raw.usage);
1751
+ return {
1752
+ ok: true,
1753
+ drafts: {
1754
+ drafts: ordered,
1755
+ findings: _chunk7THXD7ANcjs.keyPositionFinding.call(void 0,
1756
+ ordered.map((one) => one.draft),
1757
+ []
1758
+ ),
1759
+ ...provenance !== void 0 ? { provenance } : {},
1760
+ ...usage !== void 0 ? { usage } : {}
1761
+ }
1762
+ };
1763
+ }
1764
+ function checkAiDrafts(raw, request, options) {
1765
+ return check(raw, request, idSource(options.newId, "checkAiDrafts"));
1766
+ }
1767
+ function pathForModel(type, problem) {
1768
+ const { path, code } = problem;
1769
+ if (type === "gap-select" && path.length === 3 && path[0] === "gaps" && path[2] === "correctChoiceId") {
1770
+ return ["gaps", path[1], "choices"];
1771
+ }
1772
+ return code === "ra_text_long_for_time" ? ["referenceText"] : [...path];
1773
+ }
1774
+ function problemsOf(one) {
1775
+ const writes = WRITES[one.type];
1776
+ return [
1777
+ ...one.validation.issues.map(
1778
+ (found) => ({
1779
+ path: found.path,
1780
+ code: found.code,
1781
+ message: found.message,
1782
+ severity: found.severity
1783
+ })
1784
+ ),
1785
+ ...one.findings.filter((found) => found.severity === "warning").map(
1786
+ (found) => ({
1787
+ path: found.path,
1788
+ code: found.code,
1789
+ message: found.message,
1790
+ severity: "warning"
1791
+ })
1792
+ )
1793
+ ].map((problem) => ({ ...problem, path: pathForModel(one.type, problem) })).filter((problem) => writes.includes(problem.path[0]));
1794
+ }
1795
+ function aiDraftsRepairRequest(request, drafts) {
1796
+ const list = "drafts" in drafts ? drafts.drafts : drafts;
1797
+ const repair = list.flatMap((one) => {
1798
+ const problems = problemsOf(one);
1799
+ return problems.length === 0 ? [] : [{ index: one.index, draft: one.generated, problems }];
1800
+ });
1801
+ if (repair.length === 0) {
1802
+ return null;
1803
+ }
1804
+ const { repair: _previous, ...first } = request;
1805
+ return {
1806
+ ...first,
1807
+ shape: replyShape(
1808
+ request.facts.activityTypes,
1809
+ request.facts.source.kind === "captions",
1810
+ repair.length,
1811
+ repair.length
1812
+ ),
1813
+ repair
1814
+ };
1815
+ }
1816
+ var RANK = { complete: 2, incomplete: 1, invalid: 0 };
1817
+ function noWorse(repaired, before) {
1818
+ const warnings = (one) => one.findings.filter((found) => found.severity === "warning").length;
1819
+ const now = RANK[repaired.validation.status];
1820
+ const was = RANK[before.validation.status];
1821
+ return now > was || now === was && warnings(repaired) <= warnings(before);
1822
+ }
1823
+ async function generateDrafts(input) {
1824
+ const { request, port, newId } = input;
1825
+ const repairs = _nullishCoalesce(input.repairs, () => ( 1));
1826
+ if (!Number.isInteger(repairs) || repairs < 0 || repairs > AI_DRAFTS_MAX_REPAIRS) {
1827
+ throw new RangeError(
1828
+ `generateDrafts: repairs must be a whole number from 0 to ${AI_DRAFTS_MAX_REPAIRS}, not ${String(repairs)}.`
1829
+ );
1830
+ }
1831
+ const calls = [];
1832
+ const ids = idSource(newId, "generateDrafts");
1833
+ const ask = async (asked, keep) => {
1834
+ let raw;
1835
+ try {
1836
+ raw = await port(asked);
1837
+ } catch (error) {
1838
+ calls.push({ ok: false, error: error instanceof Error ? error.message : String(error) });
1839
+ return void 0;
1840
+ }
1841
+ const checked = check(raw, asked, ids, keep);
1842
+ if (!checked.ok) {
1843
+ calls.push({ ok: false, refusal: checked.refusal });
1844
+ return void 0;
1845
+ }
1846
+ const { provenance, usage } = checked.drafts;
1847
+ calls.push({
1848
+ ok: true,
1849
+ ...provenance !== void 0 ? { provenance } : {},
1850
+ ...usage !== void 0 ? { usage } : {}
1851
+ });
1852
+ return checked.drafts;
1853
+ };
1854
+ const first = await ask(request, []);
1855
+ if (first === void 0) {
1856
+ return { drafts: [], findings: [], calls };
1857
+ }
1858
+ let current = first.drafts;
1859
+ for (let pass = 0; pass < repairs; pass += 1) {
1860
+ const repairRequest = aiDraftsRepairRequest(request, current);
1861
+ if (repairRequest === null) {
1862
+ break;
1863
+ }
1864
+ const keep = (_nullishCoalesce(repairRequest.repair, () => ( []))).map((wanted) => {
1865
+ const before = current.find((one) => one.index === wanted.index);
1866
+ return typeof _optionalChain([before, 'optionalAccess', _43 => _43.draft, 'access', _44 => _44.id]) === "string" ? before.draft.id : void 0;
1867
+ });
1868
+ const repaired = await ask(repairRequest, keep);
1869
+ if (repaired === void 0) {
1870
+ break;
1871
+ }
1872
+ current = current.map((before) => {
1873
+ const after = repaired.drafts.find((one) => one.index === before.index);
1874
+ return after !== void 0 && noWorse(after, before) ? after : before;
1875
+ });
1876
+ }
1877
+ return {
1878
+ drafts: current,
1879
+ findings: _chunk7THXD7ANcjs.keyPositionFinding.call(void 0,
1880
+ current.map((one) => one.draft),
1881
+ []
1882
+ ),
1883
+ calls
1884
+ };
1885
+ }
1886
+ function sourceText(request) {
1887
+ const source = request.facts.source;
1888
+ return source.kind === "captions" ? source.captions.map((caption) => caption.text).join("\n") : source.text;
1889
+ }
1890
+ function interactiveVideoFromDrafts(input) {
1891
+ const { request, drafts, video, durationSeconds, title } = input;
1892
+ const ids = idSource(input.newId, "interactiveVideoFromDrafts");
1893
+ const types = request.facts.activityTypes;
1894
+ const list = "drafts" in drafts ? drafts.drafts : drafts;
1895
+ const end = typeof durationSeconds === "number" && Number.isFinite(durationSeconds) && durationSeconds >= 0 ? durationSeconds : void 0;
1896
+ const placed = list.map((one) => ({ one, at: _nullishCoalesce(one.at, () => ( end)) }));
1897
+ const ordered = [...placed].sort(
1898
+ (a, b) => (_nullishCoalesce(a.at, () => ( Number.POSITIVE_INFINITY))) - (_nullishCoalesce(b.at, () => ( Number.POSITIVE_INFINITY))) || types.indexOf(a.one.type) - types.indexOf(b.one.type) || a.one.index - b.one.index
1899
+ );
1900
+ const cues = [];
1901
+ for (const { one, at } of ordered) {
1902
+ const itemId = String(one.draft.id);
1903
+ const last = cues[cues.length - 1];
1904
+ if (last !== void 0 && last.at === at) {
1905
+ last.itemIds.push(itemId);
1906
+ } else {
1907
+ cues.push({ id: ids(), ...at !== void 0 ? { at } : {}, itemIds: [itemId] });
1908
+ }
1909
+ }
1910
+ const group = {
1911
+ schemaVersion: "1.0",
1912
+ type: "item-group",
1913
+ id: ids(),
1914
+ ...title !== void 0 ? { title } : {},
1915
+ stimulus: { id: ids(), kind: "video", media: video, transcript: sourceText(request) },
1916
+ items: ordered.map(({ one }) => one.draft),
1917
+ timeline: { cues }
1918
+ };
1919
+ return {
1920
+ group,
1921
+ validation: validateItemGroupDraft(group),
1922
+ findings: critiqueItemGroupDraft(group)
1923
+ };
1924
+ }
1925
+
1926
+ // src/ai-writing.ts
1927
+ var AI_WRITING_MAX_CORRECTIONS = 20;
1928
+ var AI_WRITING_MAX_FIELD_LENGTH = 500;
1929
+ var MAX_TAG_LENGTH = 40;
1930
+ function aiWritingFeedbackRequest(input) {
1931
+ const { data, response, learnerLocale } = input;
1932
+ if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) {
1933
+ return null;
1934
+ }
1935
+ const text2 = _optionalChain([response, 'optionalAccess', _45 => _45.type]) === "written-response" ? response.text : "";
1936
+ if (typeof text2 !== "string" || text2.trim() === "") {
1937
+ return null;
1938
+ }
1939
+ const item = data;
1940
+ const wordCount = _chunk7THXD7ANcjs.countWords.call(void 0, text2);
1941
+ const minWords = typeof item.minWords === "number" ? item.minWords : 0;
1942
+ const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
1943
+ const criteria = Array.isArray(_optionalChain([item, 'access', _46 => _46.rubric, 'optionalAccess', _47 => _47.criteria])) ? item.rubric.criteria : null;
1944
+ const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
1945
+ name: criterion.name,
1946
+ ...typeof criterion.description === "string" ? { description: criterion.description } : {},
1947
+ weight: criterion.weight
1948
+ }));
1949
+ const previousFeedback = [..._nullishCoalesce(input.previousFeedback, () => ( []))];
1950
+ return {
1951
+ feature: "writing-feedback",
1952
+ facts: {
1953
+ activityType: "written-response",
1954
+ activityId: data.id,
1955
+ title: data.title,
1956
+ ...typeof data.locale === "string" ? { locale: data.locale } : {},
1957
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
1958
+ text: text2,
1959
+ wordCount,
1960
+ minWords,
1961
+ maxWords,
1962
+ withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
1963
+ rubric,
1964
+ ...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
1965
+ },
1966
+ draftNumber: previousFeedback.length + 1,
1967
+ previousFeedback,
1968
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
1969
+ };
1970
+ }
1971
+ function anchor(draft, quote, claimed, taken) {
1972
+ const sought = needle(quote);
1973
+ if (sought === "") {
1974
+ return null;
1975
+ }
1976
+ if (claimed !== void 0) {
1977
+ const { start, end } = claimed;
1978
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end > draft.text.length || start >= end) {
1979
+ return null;
1980
+ }
1981
+ const span = draft.text.slice(start, end);
1982
+ if (needle(span) !== sought) {
1983
+ return null;
1984
+ }
1985
+ const lead = span.length - span.trimStart().length;
1986
+ const trail = span.length - span.trimEnd().length;
1987
+ return { start: start + lead, end: end - trail };
1988
+ }
1989
+ let first = null;
1990
+ for (let at = draft.chars.indexOf(sought); at !== -1; at = draft.chars.indexOf(sought, at + 1)) {
1991
+ const start = draft.from[at];
1992
+ const end = draft.from[at + sought.length - 1] + 1;
1993
+ const found = { start, end };
1994
+ first ??= found;
1995
+ const key = `${start}:${end}`;
1996
+ if (!taken.has(key)) {
1997
+ taken.add(key);
1998
+ return found;
1999
+ }
2000
+ }
2001
+ return first;
2002
+ }
2003
+ function textField(value, max) {
2004
+ if (value === void 0 || value === null) {
2005
+ return { ok: true, value: void 0 };
2006
+ }
2007
+ if (typeof value !== "string") {
2008
+ return { ok: false, refusal: "malformed" };
2009
+ }
2010
+ const cleaned = cleanText(value);
2011
+ if ([...cleaned].length > max) {
2012
+ return { ok: false, refusal: "too-long" };
2013
+ }
2014
+ return { ok: true, value: cleaned === "" ? void 0 : cleaned };
2015
+ }
2016
+ var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2017
+ function checkCorrections(raw, text2) {
2018
+ if (raw === void 0 || raw === null) {
2019
+ return { ok: true, corrections: [] };
2020
+ }
2021
+ if (!Array.isArray(raw)) {
2022
+ return { ok: false, refusal: "malformed" };
2023
+ }
2024
+ if (raw.length > AI_WRITING_MAX_CORRECTIONS) {
2025
+ return { ok: false, refusal: "too-long" };
2026
+ }
2027
+ const draft = { text: text2, ...folded(text2) };
2028
+ const taken = /* @__PURE__ */ new Set();
2029
+ const corrections = [];
2030
+ for (const entry of raw) {
2031
+ if (!isRecord5(entry) || typeof entry.original !== "string") {
2032
+ return { ok: false, refusal: "malformed" };
2033
+ }
2034
+ if (typeof entry.corrected !== "string") {
2035
+ return { ok: false, refusal: "malformed" };
2036
+ }
2037
+ if ([...entry.original].length > AI_WRITING_MAX_FIELD_LENGTH || [...entry.corrected].length > AI_WRITING_MAX_FIELD_LENGTH) {
2038
+ return { ok: false, refusal: "too-long" };
2039
+ }
2040
+ if (needle(entry.original) === needle(entry.corrected)) {
2041
+ return { ok: false, refusal: "malformed" };
2042
+ }
2043
+ const explanation = textField(entry.explanation, AI_WRITING_MAX_FIELD_LENGTH);
2044
+ const category = textField(entry.category, MAX_TAG_LENGTH);
2045
+ if (!explanation.ok) {
2046
+ return explanation;
2047
+ }
2048
+ if (!category.ok) {
2049
+ return category;
2050
+ }
2051
+ let claimed;
2052
+ if (entry.range !== void 0 && entry.range !== null) {
2053
+ if (!isRecord5(entry.range)) {
2054
+ return { ok: false, refusal: "malformed" };
2055
+ }
2056
+ claimed = { start: entry.range.start, end: entry.range.end };
2057
+ }
2058
+ const at = anchor(draft, entry.original, claimed, taken);
2059
+ if (at === null) {
2060
+ return { ok: false, refusal: "misquotes-answer" };
2061
+ }
2062
+ corrections.push({
2063
+ original: text2.slice(at.start, at.end),
2064
+ corrected: cleanText(entry.corrected),
2065
+ ...explanation.value !== void 0 ? { explanation: explanation.value } : {},
2066
+ ...category.value !== void 0 ? { category: category.value } : {},
2067
+ range: at
2068
+ });
2069
+ }
2070
+ return { ok: true, corrections };
2071
+ }
2072
+ function isJudgedScore(score2, maxScore) {
2073
+ if (score2 === void 0) {
2074
+ return maxScore === void 0 || typeof maxScore === "number" && maxScore > 0;
2075
+ }
2076
+ const max = _nullishCoalesce(maxScore, () => ( 1));
2077
+ return typeof score2 === "number" && typeof max === "number" && Number.isFinite(score2) && Number.isFinite(max) && max > 0 && score2 >= 0 && score2 - max <= max * 1e-9;
2078
+ }
2079
+ function checkCriteria(raw, rubric) {
2080
+ if (raw === void 0 || raw === null) {
2081
+ return { ok: true, criteria: [] };
2082
+ }
2083
+ if (!Array.isArray(raw)) {
2084
+ return { ok: false, refusal: "malformed" };
2085
+ }
2086
+ if (raw.length === 0) {
2087
+ return { ok: true, criteria: [] };
2088
+ }
2089
+ if (rubric === null) {
2090
+ return { ok: false, refusal: "malformed" };
2091
+ }
2092
+ const weights = new Map(rubric.map((criterion) => [criterion.name, criterion.weight]));
2093
+ const seen = /* @__PURE__ */ new Set();
2094
+ const criteria = [];
2095
+ for (const entry of raw) {
2096
+ if (!isRecord5(entry) || typeof entry.name !== "string") {
2097
+ return { ok: false, refusal: "malformed" };
2098
+ }
2099
+ const weight = weights.get(entry.name);
2100
+ if (weight === void 0 || seen.has(entry.name)) {
2101
+ return { ok: false, refusal: "malformed" };
2102
+ }
2103
+ seen.add(entry.name);
2104
+ if (!isJudgedScore(entry.score, entry.maxScore)) {
2105
+ return { ok: false, refusal: "malformed" };
2106
+ }
2107
+ if (entry.notApplicable !== void 0 && typeof entry.notApplicable !== "boolean") {
2108
+ return { ok: false, refusal: "malformed" };
2109
+ }
2110
+ const band = textField(entry.band, MAX_TAG_LENGTH);
2111
+ const comment = textField(entry.comment, AI_WRITING_MAX_FIELD_LENGTH);
2112
+ if (!band.ok) {
2113
+ return band;
2114
+ }
2115
+ if (!comment.ok) {
2116
+ return comment;
2117
+ }
2118
+ criteria.push({
2119
+ name: entry.name,
2120
+ ...typeof entry.score === "number" ? { score: entry.score } : {},
2121
+ ...typeof entry.maxScore === "number" ? { maxScore: entry.maxScore } : {},
2122
+ ...band.value !== void 0 ? { band: band.value } : {},
2123
+ ...comment.value !== void 0 ? { comment: comment.value } : {},
2124
+ ...entry.notApplicable === true ? { notApplicable: true } : {},
2125
+ weight
2126
+ });
2127
+ }
2128
+ return { ok: true, criteria };
2129
+ }
2130
+ function checkAiWritingFeedback(raw, request) {
2131
+ const read = readAiTextResult(raw);
2132
+ if (!read.ok) {
2133
+ return read;
2134
+ }
2135
+ const record = raw;
2136
+ const corrections = checkCorrections(record.corrections, request.facts.text);
2137
+ if (!corrections.ok) {
2138
+ return corrections;
2139
+ }
2140
+ const rubric = request.facts.rubric;
2141
+ const criteria = checkCriteria(record.criteria, rubric);
2142
+ if (!criteria.ok) {
2143
+ return criteria;
2144
+ }
2145
+ let indicativeScore = null;
2146
+ const judged = new Set(criteria.criteria.map((criterion) => criterion.name));
2147
+ if (_optionalChain([rubric, 'optionalAccess', _48 => _48.every, 'call', _49 => _49((criterion) => judged.has(criterion.name))])) {
2148
+ const grade = _chunk4YANYTFNcjs.gradeFromRubric.call(void 0, criteria.criteria);
2149
+ indicativeScore = "unscorable" in grade ? null : grade.score;
2150
+ }
2151
+ const { text: text2, provenance, usage } = read.result;
2152
+ return {
2153
+ ok: true,
2154
+ feedback: {
2155
+ text: text2,
2156
+ corrections: corrections.corrections,
2157
+ criteria: criteria.criteria,
2158
+ indicativeScore,
2159
+ ...provenance !== void 0 ? { provenance } : {},
2160
+ ...usage !== void 0 ? { usage } : {}
2161
+ }
2162
+ };
2163
+ }
2164
+
2165
+
2166
+
2167
+
2168
+
2169
+
2170
+
2171
+
2172
+
2173
+
2174
+
2175
+
2176
+
2177
+
2178
+
2179
+
2180
+
2181
+
2182
+
2183
+
2184
+
2185
+
2186
+
2187
+
2188
+
2189
+
2190
+
2191
+
2192
+
2193
+
2194
+
2195
+
2196
+
2197
+
2198
+
2199
+
2200
+
2201
+
2202
+
2203
+
2204
+
2205
+
2206
+ exports.AI_EXPLANATION_TYPES = AI_EXPLANATION_TYPES; exports.AI_HINT_TYPES = AI_HINT_TYPES; exports.AI_TEXT_MAX_LENGTH = AI_TEXT_MAX_LENGTH; exports.aiSupports = aiSupports; exports.aiAllowedByContent = aiAllowedByContent; exports.aiGradeOf = aiGradeOf; exports.buildAiFacts = buildAiFacts; exports.aiExplanationRequest = aiExplanationRequest; exports.aiHintRequest = aiHintRequest; exports.readAiTextResult = readAiTextResult; exports.checkAiExplanation = checkAiExplanation; exports.checkAiHint = checkAiHint; exports.hintRevealsAnswer = hintRevealsAnswer; exports.AI_COACHING_MAX_WORDS = AI_COACHING_MAX_WORDS; exports.AI_COACHING_MAX_TIP_LENGTH = AI_COACHING_MAX_TIP_LENGTH; exports.aiCoachingRequest = aiCoachingRequest; exports.checkAiCoaching = checkAiCoaching; exports.createItemGroupDraft = createItemGroupDraft; exports.createInteractiveVideoDraft = createInteractiveVideoDraft; exports.critiqueItemGroupDraft = critiqueItemGroupDraft; exports.critiqueDrafts = critiqueDrafts; exports.validateItemGroupDraft = validateItemGroupDraft; exports.validateDraft = validateDraft; exports.createDraft = createDraft; exports.critiqueDraft = critiqueDraft; exports.AI_CRITIQUE_MAX_FINDINGS = AI_CRITIQUE_MAX_FINDINGS; exports.AI_CRITIQUE_MAX_FIELD_LENGTH = AI_CRITIQUE_MAX_FIELD_LENGTH; exports.aiCritiqueRequest = aiCritiqueRequest; exports.checkAiCritique = checkAiCritique; exports.AI_DRAFTS_MAX_COUNT = AI_DRAFTS_MAX_COUNT; exports.AI_DRAFTS_MAX_REPAIRS = AI_DRAFTS_MAX_REPAIRS; exports.aiDraftsRequest = aiDraftsRequest; exports.checkAiDrafts = checkAiDrafts; exports.aiDraftsRepairRequest = aiDraftsRepairRequest; exports.generateDrafts = generateDrafts; exports.interactiveVideoFromDrafts = interactiveVideoFromDrafts; exports.AI_WRITING_MAX_CORRECTIONS = AI_WRITING_MAX_CORRECTIONS; exports.AI_WRITING_MAX_FIELD_LENGTH = AI_WRITING_MAX_FIELD_LENGTH; exports.aiWritingFeedbackRequest = aiWritingFeedbackRequest; exports.checkAiWritingFeedback = checkAiWritingFeedback;
2207
+ //# sourceMappingURL=chunk-MXI26H62.cjs.map