@intellectif/lk-core 1.5.0 → 1.7.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 (86) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/{ai-BYryFkF9.d.ts → ai-B1wYrqcB.d.cts} +83 -18
  3. package/dist/ai-B1wYrqcB.d.cts.map +1 -0
  4. package/dist/{ai-BYryFkF9.d.cts → ai-B1wYrqcB.d.ts} +83 -18
  5. package/dist/ai-B1wYrqcB.d.ts.map +1 -0
  6. package/dist/ai-check.cjs +13 -6
  7. package/dist/ai-check.cjs.map +1 -1
  8. package/dist/ai-check.d.cts +1 -5
  9. package/dist/ai-check.d.cts.map +1 -1
  10. package/dist/ai-check.d.ts +1 -5
  11. package/dist/ai-check.d.ts.map +1 -1
  12. package/dist/ai-check.js +13 -6
  13. package/dist/ai-check.js.map +1 -1
  14. package/dist/{ai-writing-D8PdhJJz.js → ai-writing-B7oMCt66.js} +311 -66
  15. package/dist/ai-writing-B7oMCt66.js.map +1 -0
  16. package/dist/{ai-writing-Cen7-3qs.cjs → ai-writing-DrPTV3R2.cjs} +313 -50
  17. package/dist/ai-writing-DrPTV3R2.cjs.map +1 -0
  18. package/dist/{index-DUaW1Tj5.d.ts → index-BM8LeDWn.d.cts} +2 -42
  19. package/dist/index-BM8LeDWn.d.cts.map +1 -0
  20. package/dist/{index-Bg0aeWtM.d.cts → index-BTkMQtws.d.ts} +2 -42
  21. package/dist/index-BTkMQtws.d.ts.map +1 -0
  22. package/dist/{index-wMc1E_aM.d.ts → index-BU99GFTl.d.ts} +2 -8
  23. package/dist/index-BU99GFTl.d.ts.map +1 -0
  24. package/dist/{index-CxpiWedW.d.ts → index-BdXs4-en.d.ts} +42 -20
  25. package/dist/index-BdXs4-en.d.ts.map +1 -0
  26. package/dist/{index-qMbxed1B.d.cts → index-DP9rvyOz.d.cts} +42 -20
  27. package/dist/index-DP9rvyOz.d.cts.map +1 -0
  28. package/dist/{index-D7R3Ll8R.d.cts → index-XCCoPoTU.d.cts} +2 -8
  29. package/dist/index-XCCoPoTU.d.cts.map +1 -0
  30. package/dist/index.cjs +70 -21
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +135 -69
  33. package/dist/index.d.cts.map +1 -1
  34. package/dist/index.d.ts +135 -69
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +65 -22
  37. package/dist/index.js.map +1 -1
  38. package/dist/{item-group-DEB7nLID.cjs → item-group-8P3WFuUK.cjs} +5 -6
  39. package/dist/item-group-8P3WFuUK.cjs.map +1 -0
  40. package/dist/{item-group-DdSUCUjM.js → item-group-CeDk7mfM.js} +5 -6
  41. package/dist/item-group-CeDk7mfM.js.map +1 -0
  42. package/dist/{registry-DaZRpqjW.js → registry-D92hz9iA.js} +124 -106
  43. package/dist/registry-D92hz9iA.js.map +1 -0
  44. package/dist/{registry-Ce00A_OW.cjs → registry-hzrkTeIT.cjs} +142 -107
  45. package/dist/registry-hzrkTeIT.cjs.map +1 -0
  46. package/dist/{schemas-DksueYsu.cjs → schemas-BWeskONy.cjs} +79 -18
  47. package/dist/schemas-BWeskONy.cjs.map +1 -0
  48. package/dist/{schemas-BioKFf3F.js → schemas-C7TViWCg.js} +74 -19
  49. package/dist/schemas-C7TViWCg.js.map +1 -0
  50. package/dist/schemas.cjs +5 -2
  51. package/dist/schemas.d.cts +2 -2
  52. package/dist/schemas.d.ts +2 -2
  53. package/dist/schemas.js +4 -3
  54. package/dist/{scoring-lKQVYsxy.cjs → scoring-CtkCFyFT.cjs} +22 -42
  55. package/dist/{scoring-lKQVYsxy.cjs.map → scoring-CtkCFyFT.cjs.map} +1 -1
  56. package/dist/{scoring-BKNNTz-s.js → scoring-DD1GHcLI.js} +22 -42
  57. package/dist/{scoring-BKNNTz-s.js.map → scoring-DD1GHcLI.js.map} +1 -1
  58. package/dist/scoring.cjs +2 -2
  59. package/dist/scoring.d.cts +2 -2
  60. package/dist/scoring.d.ts +2 -2
  61. package/dist/scoring.js +2 -2
  62. package/dist/{xapi-NYrTlpID.cjs → xapi-7btSpx6v.cjs} +5 -8
  63. package/dist/{xapi-NYrTlpID.cjs.map → xapi-7btSpx6v.cjs.map} +1 -1
  64. package/dist/{xapi-wR6gu4qS.js → xapi-Cgicg-zU.js} +5 -8
  65. package/dist/{xapi-wR6gu4qS.js.map → xapi-Cgicg-zU.js.map} +1 -1
  66. package/dist/xapi.cjs +1 -1
  67. package/dist/xapi.d.cts +1 -1
  68. package/dist/xapi.d.ts +1 -1
  69. package/dist/xapi.js +1 -1
  70. package/package.json +2 -2
  71. package/dist/ai-BYryFkF9.d.cts.map +0 -1
  72. package/dist/ai-BYryFkF9.d.ts.map +0 -1
  73. package/dist/ai-writing-Cen7-3qs.cjs.map +0 -1
  74. package/dist/ai-writing-D8PdhJJz.js.map +0 -1
  75. package/dist/index-Bg0aeWtM.d.cts.map +0 -1
  76. package/dist/index-CxpiWedW.d.ts.map +0 -1
  77. package/dist/index-D7R3Ll8R.d.cts.map +0 -1
  78. package/dist/index-DUaW1Tj5.d.ts.map +0 -1
  79. package/dist/index-qMbxed1B.d.cts.map +0 -1
  80. package/dist/index-wMc1E_aM.d.ts.map +0 -1
  81. package/dist/item-group-DEB7nLID.cjs.map +0 -1
  82. package/dist/item-group-DdSUCUjM.js.map +0 -1
  83. package/dist/registry-Ce00A_OW.cjs.map +0 -1
  84. package/dist/registry-DaZRpqjW.js.map +0 -1
  85. package/dist/schemas-BioKFf3F.js.map +0 -1
  86. package/dist/schemas-DksueYsu.cjs.map +0 -1
@@ -1,7 +1,7 @@
1
- import { B as covers, F as readRubricLevels, G as issue, H as isRecord$5, I as countWords, K as pathKey, L as DRAFT_ISSUE_SEVERITY, M as LEVEL_PROBABILITY_TOLERANCE, N as levelDistribution, O as keyPositionFinding, P as levelKey, R as checkMedia, U as isUnset, V as isMissingId, W as isUnwritten, X as groupCaptionsField, Y as GROUP_CAPTIONS_REVEAL_DICTATION, _t as fold, a as readSchema, bt as revealsAnswer, et as UnknownActivityTypeError, ft as withValidationScope, gt as containsWords, k as normaliseFinding, nt as alignDictation, q as refusesEmpty, t as getActivityTypeDescriptor, vt as numberedPassage, yt as placeholderOrder, z as coveredPathsOf } from "./registry-DaZRpqjW.js";
2
- import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-BKNNTz-s.js";
3
- import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-DdSUCUjM.js";
4
- //#region src/ai-material.ts
1
+ import { A as normaliseFinding, B as coveredPathsOf, F as levelKey, G as isUnwritten, H as isMissingId, I as readRubricLevels, J as refusesEmpty, K as issue, L as countWords, N as LEVEL_PROBABILITY_TOLERANCE, O as keyPositionFinding, P as levelDistribution, R as DRAFT_ISSUE_SEVERITY, U as isRecord$6, V as covers, W as isUnset, X as GROUP_CAPTIONS_REVEAL_DICTATION, Z as groupCaptionsField, _t as containsWords, a as readSchema, bt as placeholderOrder, k as finding, pt as withValidationScope, q as pathKey, rt as alignDictation, t as getActivityTypeDescriptor, tt as UnknownActivityTypeError, vt as fold, xt as revealsAnswer, yt as numberedPassage, z as checkMedia } from "./registry-D92hz9iA.js";
2
+ import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-DD1GHcLI.js";
3
+ import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-CeDk7mfM.js";
4
+ // src/ai-material.ts
5
5
  /**
6
6
  * What a model is told about the group a question sits in: the material the
7
7
  * learner reads and sees beside it.
@@ -28,7 +28,7 @@ const MEDIA_TYPES = {
28
28
  video: true,
29
29
  embed: true
30
30
  };
31
- const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
31
+ const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
32
32
  /** A string with something in it, as written; otherwise nothing. */
33
33
  const filled = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
34
34
  /**
@@ -62,17 +62,17 @@ function cut(text) {
62
62
  * and a signed address must not end up in a prompt. Each field is read once.
63
63
  */
64
64
  function materialFacts(group) {
65
- if (!isRecord$4(group)) return null;
65
+ if (!isRecord$5(group)) return null;
66
66
  const stimulus = group.stimulus;
67
- if (!isRecord$4(stimulus)) return null;
67
+ if (!isRecord$5(stimulus)) return null;
68
68
  const kind = stimulus.kind;
69
69
  if (typeof kind !== "string" || !Object.hasOwn(KINDS$2, kind)) return null;
70
70
  const title = filled(stimulus.title) ?? filled(group.title);
71
71
  const body = filled(stimulus.body);
72
72
  const text = body === void 0 ? void 0 : cut(body);
73
73
  const rawMedia = stimulus.media;
74
- const mediaType = isRecord$4(rawMedia) ? rawMedia.type : void 0;
75
- const alt = isRecord$4(rawMedia) ? filled(rawMedia.alt) : void 0;
74
+ const mediaType = isRecord$5(rawMedia) ? rawMedia.type : void 0;
75
+ const alt = isRecord$5(rawMedia) ? filled(rawMedia.alt) : void 0;
76
76
  const media = typeof mediaType === "string" && Object.hasOwn(MEDIA_TYPES, mediaType) ? {
77
77
  type: mediaType,
78
78
  ...alt !== void 0 ? { alt } : {}
@@ -89,8 +89,7 @@ function materialFacts(group) {
89
89
  ...locale !== void 0 ? { locale } : {}
90
90
  };
91
91
  }
92
- //#endregion
93
- //#region src/ai-text.ts
92
+ // src/ai-text.ts
94
93
  /**
95
94
  * How the SDK reads text a model wrote, before a learner does. Internal: shared
96
95
  * by the AI checks, and exported by none of the package's entry points.
@@ -178,8 +177,7 @@ function folded(text) {
178
177
  }
179
178
  /** A quote as it is searched for: folded, and without the spaces around it. */
180
179
  const needle = (quote) => folded(quote).chars.trim();
181
- //#endregion
182
- //#region src/ai.ts
180
+ // src/ai.ts
183
181
  /**
184
182
  * The activity types the SDK builds facts for, and which of them take hints.
185
183
  * A dictation takes none: its answer is the whole sentence, and it has
@@ -284,6 +282,11 @@ const base = (data) => ({
284
282
  title: data.title,
285
283
  ...typeof data.locale === "string" ? { locale: data.locale } : {}
286
284
  });
285
+ /** The item's instructions, trimmed; nothing for none, or for blank ones the learner never sees. */
286
+ function instructionsOf$1(data) {
287
+ const text = data.instructions;
288
+ return typeof text === "string" && text.trim() !== "" ? { instructions: text.trim() } : {};
289
+ }
287
290
  /**
288
291
  * The facts a model is given about one item and one answer. `details` are the
289
292
  * SDK's per-part verdicts — from its own scorer, or from a server's outcome in
@@ -303,6 +306,7 @@ function buildAiFacts(data, response, details) {
303
306
  return {
304
307
  ...base(data),
305
308
  activityType: "multiple-choice",
309
+ ...instructionsOf$1(data),
306
310
  question: item.question,
307
311
  mode: item.mode,
308
312
  options: item.options.map((option) => {
@@ -328,6 +332,7 @@ function buildAiFacts(data, response, details) {
328
332
  return {
329
333
  ...base(data),
330
334
  activityType: "fill-in-the-blanks",
335
+ ...instructionsOf$1(data),
331
336
  passage: numberedPassage(item.passage),
332
337
  blanks: placeholderOrder(item.passage).map((id, index) => {
333
338
  const blank = blanks.get(id);
@@ -353,6 +358,7 @@ function buildAiFacts(data, response, details) {
353
358
  return {
354
359
  ...base(data),
355
360
  activityType: "gap-select",
361
+ ...instructionsOf$1(data),
356
362
  passage: numberedPassage(item.passage),
357
363
  gaps: placeholderOrder(item.passage).map((id, index) => {
358
364
  const gap = gaps.get(id);
@@ -596,8 +602,7 @@ function hintRevealsAnswer(facts, hint) {
596
602
  default: return true;
597
603
  }
598
604
  }
599
- //#endregion
600
- //#region src/ai-coaching.ts
605
+ // src/ai-coaching.ts
601
606
  /** The most words one coaching may work on. More is refused, not cut. */
602
607
  const AI_COACHING_MAX_WORDS = 20;
603
608
  /** The longest tip for one word, in code points. */
@@ -742,7 +747,7 @@ function aiCoachingRequest(input) {
742
747
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
743
748
  };
744
749
  }
745
- const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
750
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
746
751
  /** A symbol as it is compared: composed, and without the spaces around it. */
747
752
  const symbolKey = (symbol) => symbol.normalize("NFC").trim();
748
753
  /**
@@ -791,7 +796,7 @@ function checkAiCoaching(raw, request) {
791
796
  });
792
797
  const seen = /* @__PURE__ */ new Set();
793
798
  for (const entry of entries) {
794
- if (!isRecord$3(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") return {
799
+ if (!isRecord$4(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") return {
795
800
  ok: false,
796
801
  refusal: "malformed"
797
802
  };
@@ -839,7 +844,7 @@ function checkAiCoaching(raw, request) {
839
844
  /** A sound a model names, checked against the sounds the engine reported for the word. */
840
845
  function checkSound(raw, fact) {
841
846
  if (raw === void 0 || raw === null) return;
842
- if (!isRecord$3(raw) || typeof raw.expected !== "string" || raw.heard !== void 0 && raw.heard !== null && typeof raw.heard !== "string") return {
847
+ if (!isRecord$4(raw) || typeof raw.expected !== "string" || raw.heard !== void 0 && raw.heard !== null && typeof raw.heard !== "string") return {
843
848
  ok: false,
844
849
  refusal: "malformed"
845
850
  };
@@ -865,8 +870,239 @@ function checkSound(raw, fact) {
865
870
  heard: instead.symbol
866
871
  };
867
872
  }
868
- //#endregion
869
- //#region src/authoring/item-group.ts
873
+ // src/media-refs.ts
874
+ const MAIN_KINDS = [
875
+ "audio",
876
+ "video",
877
+ "image",
878
+ "embed"
879
+ ];
880
+ function isRecord$3(value) {
881
+ return typeof value === "object" && value !== null && !Array.isArray(value);
882
+ }
883
+ function valueAt(root, path) {
884
+ let at = root;
885
+ for (const key of path) {
886
+ if (typeof key === "number" ? !Array.isArray(at) : !isRecord$3(at)) return;
887
+ at = at[key];
888
+ }
889
+ return at;
890
+ }
891
+ /** Where `item` keeps its files: what its type declares, or its `media` alone. */
892
+ function slotsOf(item) {
893
+ const declared = typeof item.type === "string" ? getActivityTypeDescriptor(item.type)?.mediaSlots : void 0;
894
+ if (declared === void 0) return [{ path: ["media"] }];
895
+ const slots = declared(item);
896
+ return Array.isArray(slots) ? slots : [];
897
+ }
898
+ function holderRefs(holder, path, kindOfSlot, owner) {
899
+ if (!isRecord$3(holder)) return [];
900
+ const main = typeof holder.type === "string" && MAIN_KINDS.includes(holder.type) ? holder.type : kindOfSlot;
901
+ const refs = [];
902
+ const add = (field, kind, url) => {
903
+ refs.push({
904
+ path: [...path, ...field],
905
+ kind,
906
+ url,
907
+ holder,
908
+ ...owner
909
+ });
910
+ };
911
+ if (main !== void 0) add(["url"], main, typeof holder.url === "string" ? holder.url : null);
912
+ if (typeof holder.captionsUrl === "string") add(["captionsUrl"], "captions", holder.captionsUrl);
913
+ if (Array.isArray(holder.tracks)) holder.tracks.forEach((track, index) => {
914
+ if (isRecord$3(track) && typeof track.src === "string") refs.push({
915
+ path: [
916
+ ...path,
917
+ "tracks",
918
+ index,
919
+ "src"
920
+ ],
921
+ kind: "track",
922
+ url: track.src,
923
+ holder: track,
924
+ ...owner,
925
+ ...typeof track.srclang === "string" ? { srclang: track.srclang } : {}
926
+ });
927
+ });
928
+ if (typeof holder.poster === "string") add(["poster"], "poster", holder.poster);
929
+ return refs;
930
+ }
931
+ function activityRefs(item, base) {
932
+ if (!isRecord$3(item)) return [];
933
+ const owner = typeof item.id === "string" ? { activityId: item.id } : {};
934
+ return slotsOf(item).flatMap((slot) => holderRefs(valueAt(item, slot.path), [...base, ...slot.path], slot.kind, owner));
935
+ }
936
+ /**
937
+ * Every media address in one piece of content: a loose activity or an item
938
+ * group (its stimulus first, then each item in authored order), as a
939
+ * `SequenceEntry` holds them. Drafts are read too.
940
+ *
941
+ * For each activity the walker reads the places its type keeps files: an
942
+ * item's `media`, each multiple-choice option's `media`, and dictation's and
943
+ * read-aloud's `slowMedia`. In each it lists the main file, `captionsUrl`,
944
+ * every track's `src` and `poster`. A type you register declares its own with
945
+ * `mediaSlots`; without that, its `media` is read.
946
+ *
947
+ * A main file with no address yet is listed with `url: null`, so a recording
948
+ * still to be made has a place in the list. Not read: addresses inside an
949
+ * `…Html` sidecar (the SDK does not parse HTML) and a learner's recordings,
950
+ * which are responses rather than content.
951
+ *
952
+ * ```ts
953
+ * const files = listMediaRefs(entry).filter((ref) => ref.url !== null);
954
+ * ```
955
+ */
956
+ function listMediaRefs(entry) {
957
+ if (!isRecord$3(entry)) return [];
958
+ if (entry.type !== "item-group") return activityRefs(entry, []);
959
+ const refs = [];
960
+ const stimulus = entry.stimulus;
961
+ if (isRecord$3(stimulus)) {
962
+ const owner = typeof stimulus.id === "string" ? { stimulusId: stimulus.id } : {};
963
+ refs.push(...holderRefs(stimulus.media, ["stimulus", "media"], void 0, owner));
964
+ }
965
+ if (Array.isArray(entry.items)) entry.items.forEach((item, index) => {
966
+ refs.push(...activityRefs(item, ["items", index]));
967
+ });
968
+ return refs;
969
+ }
970
+ /** A deep copy of a JSON value. Content is plain data; a cycle cannot be copied. */
971
+ function copyOf(value, seen) {
972
+ if (typeof value !== "object" || value === null) return value;
973
+ if (seen.has(value)) throw new TypeError("mapMediaRefs: the content contains a circular reference.");
974
+ seen.add(value);
975
+ try {
976
+ if (Array.isArray(value)) return value.map((element) => copyOf(element, seen));
977
+ const copy = {};
978
+ for (const [key, field] of Object.entries(value)) copy[key] = copyOf(field, seen);
979
+ return copy;
980
+ } finally {
981
+ seen.delete(value);
982
+ }
983
+ }
984
+ /**
985
+ * A copy of `entry` with each address {@link listMediaRefs} finds replaced
986
+ * by what `fn` returns for it: files moved into a new folder structure, or an
987
+ * export's file names pointed at where an import stored them. Nothing else
988
+ * changes, and `entry` is never modified: with `(ref) => ref.url`, the copy is
989
+ * deep-equal to it, with the same `canonicalJson`.
990
+ *
991
+ * For a main file with no address yet (`url: null`), a string attaches one
992
+ * and `null` leaves it empty. For an address that is there, `fn` must return
993
+ * a string: removing a file is not mapping.
994
+ *
995
+ * The copy validates as the original did when every address `fn` returns is
996
+ * one the media rule accepts: `https:`, `http:`, `data:`, `blob:`, or
997
+ * root-relative (`/…`). A bundle-relative `media/part1.mp3` is refused.
998
+ *
999
+ * @throws TypeError when `fn` returns something other than a string or
1000
+ * `null`, or `null` for an address that is there, or when `entry` is circular.
1001
+ */
1002
+ function mapMediaRefs(entry, fn) {
1003
+ const refs = listMediaRefs(entry);
1004
+ const copy = copyOf(entry, /* @__PURE__ */ new Set());
1005
+ for (const ref of refs) {
1006
+ const next = fn(ref);
1007
+ if (next === null) {
1008
+ if (ref.url !== null) throw new TypeError(`mapMediaRefs: ${ref.path.join(".")} has a file; returning null would remove it, which is not mapping.`);
1009
+ continue;
1010
+ }
1011
+ if (typeof next !== "string") throw new TypeError(`mapMediaRefs: the function must return a string or null for ${ref.path.join(".")}, not ${typeof next}.`);
1012
+ if (next === ref.url) continue;
1013
+ const holder = valueAt(copy, ref.path.slice(0, -1));
1014
+ holder[ref.path[ref.path.length - 1]] = next;
1015
+ }
1016
+ return copy;
1017
+ }
1018
+ /** A holder's own text: its transcript, else its script as "Speaker: text" lines. */
1019
+ function ownText(holder) {
1020
+ if (typeof holder.transcript === "string" && holder.transcript.trim() !== "") return holder.transcript;
1021
+ const turns = Array.isArray(holder.script) ? holder.script.flatMap((turn) => isRecord$3(turn) && typeof turn.speaker === "string" && typeof turn.text === "string" && turn.text.trim() !== "" ? [`${turn.speaker.trim()}: ${turn.text.trim()}`] : []) : [];
1022
+ return turns.length > 0 ? turns.join("\n") : null;
1023
+ }
1024
+ /**
1025
+ * The author-side text of a recording: its `transcript`, or else its
1026
+ * `script` as one "Speaker: text" line per turn. `null` when it has neither.
1027
+ *
1028
+ * `holder` is a media object, a stimulus, or an activity. A stimulus or an
1029
+ * activity is read for its own text first and then for its `media`'s; a
1030
+ * stimulus keeps both at its own level. Author-only: pass it to the author's
1031
+ * requests (`aiDraftsRequest`'s `source`, a critique), never to a learner's.
1032
+ *
1033
+ * ```ts
1034
+ * const text = transcriptOf(item.media);
1035
+ * if (text !== null) aiDraftsRequest({ types: ['multiple-choice'], source: { kind: 'transcript', text } });
1036
+ * ```
1037
+ */
1038
+ function transcriptOf(holder) {
1039
+ if (!isRecord$3(holder)) return null;
1040
+ return ownText(holder) ?? (isRecord$3(holder.media) ? ownText(holder.media) : null);
1041
+ }
1042
+ // src/authoring/recordings.ts
1043
+ const written$1 = (value) => typeof value === "string" && value.trim() !== "";
1044
+ /** The script's turns that have a speaker and words, or none. */
1045
+ function turnsOf(script) {
1046
+ return Array.isArray(script) ? script.flatMap((turn) => isRecord$6(turn) && typeof turn.speaker === "string" && written$1(turn.text) ? [{
1047
+ speaker: turn.speaker,
1048
+ text: turn.text
1049
+ }] : []) : [];
1050
+ }
1051
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1052
+ /** A transcript's words, with each `Speaker:` label of the script's speakers taken out. */
1053
+ function transcriptWords(transcript, speakers) {
1054
+ const labels = [...new Set(speakers.map((speaker) => speaker.trim()).filter(written$1))];
1055
+ const unlabelled = labels.length === 0 ? transcript : transcript.replace(new RegExp(`(^|\\s)(?:${labels.map(escaped).join("|")})\\s*:`, "giu"), "$1");
1056
+ return fold(unlabelled);
1057
+ }
1058
+ /** What a recording says, as words: its transcript, or else its script's turns. */
1059
+ function spokenWords(holder) {
1060
+ const turns = turnsOf(holder.script);
1061
+ if (written$1(holder.transcript)) return transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
1062
+ return turns.length > 0 ? fold(turns.map((turn) => turn.text).join(" ")) : null;
1063
+ }
1064
+ const sameWords = (a, b) => a.length === b.length && a.every((word, index) => word === b[index]);
1065
+ /** One object's own rules: text on media with nothing to say, and a transcript its script contradicts. */
1066
+ function holderFindings(holder, path) {
1067
+ if (!isRecord$6(holder)) return [];
1068
+ const findings = [];
1069
+ const turns = turnsOf(holder.script);
1070
+ if ((written$1(holder.transcript) || turns.length > 0) && (holder.type === "image" || holder.type === "embed")) findings.push(finding("media_transcript_unused", [...path, written$1(holder.transcript) ? "transcript" : "script"], `A transcript or a script on ${holder.type === "image" ? "a picture" : "an embedded player"} has nothing to describe, and nothing reads it. Put it on the recording it belongs to.`));
1071
+ if (written$1(holder.transcript) && turns.length > 0) {
1072
+ const fromTranscript = transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
1073
+ if (!sameWords(fromTranscript, fold(turns.map((turn) => turn.text).join(" ")))) findings.push(finding("media_script_differs", [...path, "script"], "The script and the transcript say different words. AI facts read the transcript, and a voice reads the script: make them say the same."));
1074
+ }
1075
+ return findings;
1076
+ }
1077
+ /** The field a type's own recording reads out, for the types that have one. */
1078
+ const SPOKEN_FIELD = {
1079
+ dictation: "transcript",
1080
+ "read-aloud": "referenceText"
1081
+ };
1082
+ /**
1083
+ * An item's recording rules, at every place its type keeps media. A
1084
+ * dictation's and a read-aloud's own text is what their recording says, so a
1085
+ * recording that says other words is reported too.
1086
+ */
1087
+ function recordingFindings(item) {
1088
+ const findings = listMediaRefs(item).filter((ref) => ref.path[ref.path.length - 1] === "url").flatMap((ref) => holderFindings(ref.holder, ref.path.slice(0, -1)));
1089
+ const field = typeof item.type === "string" ? SPOKEN_FIELD[item.type] : void 0;
1090
+ const media = item.media;
1091
+ if (field === void 0 || !isRecord$6(media)) return findings;
1092
+ const said = spokenWords(media);
1093
+ const accepted = [item[field], ...item.type === "dictation" && Array.isArray(item.acceptedTranscripts) ? item.acceptedTranscripts : []].filter(written$1);
1094
+ if (said !== null && accepted.length > 0 && !accepted.some((text) => sameWords(said, fold(text)))) findings.push(finding("media_transcript_differs", ["media", written$1(media.transcript) ? "transcript" : "script"], `The recording's transcript says different words from the item's \`${field}\`, which is what the recording is meant to say.`));
1095
+ return findings;
1096
+ }
1097
+ /** A group's material: its own transcript against its script, and its media's rules. */
1098
+ function stimulusFindings(stimulus) {
1099
+ if (!isRecord$6(stimulus)) return [];
1100
+ return [...holderFindings({
1101
+ transcript: stimulus.transcript,
1102
+ script: stimulus.script
1103
+ }, ["stimulus"]), ...holderFindings(stimulus.media, ["stimulus", "media"])];
1104
+ }
1105
+ // src/authoring/item-group.ts
870
1106
  /**
871
1107
  * A new, empty reading or listening testlet for an editor to start from.
872
1108
  *
@@ -961,9 +1197,13 @@ function createInteractiveVideoDraft(context) {
961
1197
  * {@link validateItemGroupDraft} already reports it.
962
1198
  */
963
1199
  function critiqueItemGroupDraft(draft) {
964
- if (!isRecord$5(draft)) return [];
1200
+ if (!isRecord$6(draft)) return [];
965
1201
  const items = Array.isArray(draft.items) ? draft.items : [];
966
- return withValidationScope(() => [...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])), ...keyPositionFinding(items, ["items"])]);
1202
+ return withValidationScope(() => [
1203
+ ...stimulusFindings(draft.stimulus),
1204
+ ...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])),
1205
+ ...keyPositionFinding(items, ["items"])
1206
+ ]);
967
1207
  }
968
1208
  /**
969
1209
  * The item critic over a question set, as `<ActivitySequence>` takes one: each
@@ -973,11 +1213,11 @@ function critiqueItemGroupDraft(draft) {
973
1213
  * skipped.
974
1214
  */
975
1215
  function critiqueDrafts(entries) {
976
- return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$5(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
1216
+ return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$6(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
977
1217
  }
978
1218
  /** One item's findings, or none for an item the critic cannot place. */
979
1219
  function critiqueItem(item) {
980
- if (!isRecord$5(item) || typeof item.type !== "string") return [];
1220
+ if (!isRecord$6(item) || typeof item.type !== "string") return [];
981
1221
  if (getActivityTypeDescriptor(item.type) === void 0) return [];
982
1222
  return critiqueDraft(item.type, item);
983
1223
  }
@@ -990,7 +1230,7 @@ function validateItemGroupDraft(draft) {
990
1230
  }
991
1231
  function validateItemGroupDraftInScope(draft) {
992
1232
  const issues = [];
993
- if (!isRecord$5(draft)) {
1233
+ if (!isRecord$6(draft)) {
994
1234
  issues.push(issue("ig_not_an_object", [], "A group must be an object with a stimulus and its items."));
995
1235
  return {
996
1236
  status: "invalid",
@@ -998,7 +1238,7 @@ function validateItemGroupDraftInScope(draft) {
998
1238
  };
999
1239
  }
1000
1240
  issues.push(...checkGroupIdentity(draft));
1001
- if (isRecord$5(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
1241
+ if (isRecord$6(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
1002
1242
  else if (isUnset(draft.stimulus)) issues.push(issue("ig_stimulus_required", ["stimulus"], "Add the passage, recording or image the questions are about."));
1003
1243
  issues.push(...checkItems(draft));
1004
1244
  if (draft.timeline !== void 0 && draft.timeline !== null) issues.push(...checkTimeline(draft));
@@ -1048,10 +1288,10 @@ function validateItemGroupDraftInScope(draft) {
1048
1288
  function checkTimeline(draft) {
1049
1289
  const issues = [];
1050
1290
  const timeline = draft.timeline;
1051
- if (!isRecord$5(timeline)) return issues;
1052
- const stimulus = isRecord$5(draft.stimulus) ? draft.stimulus : void 0;
1291
+ if (!isRecord$6(timeline)) return issues;
1292
+ const stimulus = isRecord$6(draft.stimulus) ? draft.stimulus : void 0;
1053
1293
  if (stimulus !== void 0 && !isUnwritten(stimulus.kind) && stimulus.kind !== "video") issues.push(issue("ig_timeline_stimulus_kind", ["stimulus", "kind"], "An interactive video needs a video: quizzes open at moments of it."));
1054
- else if (stimulus !== void 0 && isRecord$5(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
1294
+ else if (stimulus !== void 0 && isRecord$6(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
1055
1295
  "stimulus",
1056
1296
  "media",
1057
1297
  "type"
@@ -1060,7 +1300,7 @@ function checkTimeline(draft) {
1060
1300
  const items = Array.isArray(draft.items) ? draft.items : [];
1061
1301
  const itemIndexById = /* @__PURE__ */ new Map();
1062
1302
  items.forEach((item, index) => {
1063
- if (!isRecord$5(item)) return;
1303
+ if (!isRecord$6(item)) return;
1064
1304
  if (typeof item.id === "string" && item.id !== "") itemIndexById.set(item.id, index);
1065
1305
  const type = item.type;
1066
1306
  if (typeof type === "string" && type !== "" && type !== "item-group" && !isInteractiveVideoItemType(type) && getActivityTypeDescriptor(type) !== void 0) issues.push(issue("ig_timeline_item_type", [
@@ -1078,7 +1318,7 @@ function checkTimeline(draft) {
1078
1318
  const quizIds = /* @__PURE__ */ new Set();
1079
1319
  const placed = /* @__PURE__ */ new Set();
1080
1320
  cues.forEach((cue, cueIndex) => {
1081
- if (!isRecord$5(cue)) return;
1321
+ if (!isRecord$6(cue)) return;
1082
1322
  const at = (field, ...rest) => [
1083
1323
  "timeline",
1084
1324
  "cues",
@@ -1109,7 +1349,7 @@ function checkTimeline(draft) {
1109
1349
  const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
1110
1350
  let previousAt;
1111
1351
  chapters.forEach((chapter, index) => {
1112
- if (!isRecord$5(chapter)) return;
1352
+ if (!isRecord$6(chapter)) return;
1113
1353
  if (isUnwritten(chapter.title)) issues.push(issue("ig_timeline_chapter_title", [
1114
1354
  "timeline",
1115
1355
  "chapters",
@@ -1170,14 +1410,14 @@ function checkStimulus(stimulus) {
1170
1410
  if (isUnwritten(kind)) issues.push(issue("ig_stimulus_kind_required", at("kind"), "Choose what the questions are about: a text, a recording, a video, an image, or a mix."));
1171
1411
  else if (typeof kind !== "string" || !KINDS$1.has(kind)) {
1172
1412
  if (typeof kind === "string") issues.push(issue("ig_stimulus_kind_invalid", at("kind"), `"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`));
1173
- if (isRecord$5(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
1413
+ if (isRecord$6(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
1174
1414
  return issues;
1175
1415
  }
1176
1416
  const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
1177
1417
  const htmlPresent = !isUnset(stimulus.bodyHtml);
1178
1418
  if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) issues.push(issue("ig_stimulus_body_required", at("body"), isUnwritten(stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."));
1179
1419
  if (kind !== "text" && isUnset(stimulus.media)) issues.push(issue("ig_stimulus_media_required", at("media"), "Add the recording, video or image."));
1180
- else if (isRecord$5(stimulus.media)) {
1420
+ else if (isRecord$6(stimulus.media)) {
1181
1421
  const allowed = MEDIA_FOR_KIND[kind];
1182
1422
  const mediaType = stimulus.media.type;
1183
1423
  if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) issues.push(issue("ig_stimulus_media_kind", at("media", "type"), `A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`));
@@ -1204,7 +1444,7 @@ function checkItems(draft) {
1204
1444
  const seen = /* @__PURE__ */ new Set();
1205
1445
  const repeated = /* @__PURE__ */ new Set();
1206
1446
  for (const [index, item] of items.entries()) {
1207
- if (!isRecord$5(item)) continue;
1447
+ if (!isRecord$6(item)) continue;
1208
1448
  const ordinal = index + 1;
1209
1449
  if (isMissingId(item.id)) issues.push(issue("ig_item_id_required", [
1210
1450
  "items",
@@ -1256,8 +1496,7 @@ function checkItems(draft) {
1256
1496
  for (const id of repeated) issues.push(issue("ig_item_id_duplicate", ["items"], `More than one question has the id "${id}".`));
1257
1497
  return issues;
1258
1498
  }
1259
- //#endregion
1260
- //#region src/authoring/index.ts
1499
+ // src/authoring/index.ts
1261
1500
  /**
1262
1501
  * Tells a draft that is not finished from a draft that is wrong.
1263
1502
  *
@@ -1301,7 +1540,7 @@ function validateDraft(type, draft) {
1301
1540
  function validateDraftInScope(type, draft) {
1302
1541
  const descriptor = getActivityTypeDescriptor(type);
1303
1542
  if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
1304
- const reported = isRecord$5(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
1543
+ const reported = isRecord$6(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
1305
1544
  const issues = [...reported];
1306
1545
  const parsed = readSchema(descriptor.schema, draft, `Activity type "${descriptor.type}"`, { reportInput: true });
1307
1546
  if (!parsed.success) {
@@ -1410,12 +1649,11 @@ function normalise(found) {
1410
1649
  function critiqueDraft(type, draft) {
1411
1650
  const descriptor = getActivityTypeDescriptor(type);
1412
1651
  if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
1652
+ if (!isRecord$6(draft)) return [];
1413
1653
  const critique = descriptor.authoring?.critique;
1414
- if (!isRecord$5(draft) || critique === void 0) return [];
1415
- return withValidationScope(() => critique(draft)).map(normaliseFinding);
1654
+ return withValidationScope(() => [...critique === void 0 ? [] : critique(draft), ...recordingFindings(draft)]).map(normaliseFinding);
1416
1655
  }
1417
- //#endregion
1418
- //#region src/ai-critique.ts
1656
+ // src/ai-critique.ts
1419
1657
  /** The most findings one critique may carry. More is refused, not cut. */
1420
1658
  const AI_CRITIQUE_MAX_FINDINGS = 20;
1421
1659
  /** The longest message or quote in a finding, in code points. */
@@ -1448,6 +1686,10 @@ const NOT_WORDS = /* @__PURE__ */ new Set([
1448
1686
  "bankId",
1449
1687
  "url",
1450
1688
  "captionsUrl",
1689
+ "src",
1690
+ "srclang",
1691
+ "poster",
1692
+ "speaker",
1451
1693
  "match",
1452
1694
  "playback",
1453
1695
  "recording",
@@ -1459,10 +1701,10 @@ const NOT_WORDS = /* @__PURE__ */ new Set([
1459
1701
  "timeline"
1460
1702
  ]);
1461
1703
  /**
1462
- * Every string an author wrote into an item, with its path: the question, the
1463
- * options, the passage, the answers, the hints, the feedback, a picture's
1464
- * `alt`. Settings, ids and addresses are left out, and so is any `…Html`
1465
- * sidecar: the plain field beside it holds the same words.
1704
+ * Every string an author wrote into an item, with its path: the instructions,
1705
+ * the question, the options, the passage, the answers, the hints, the
1706
+ * feedback, a picture's `alt`. Settings, ids and addresses are left out, and
1707
+ * so is any `…Html` sidecar: the plain field beside it holds the same words.
1466
1708
  */
1467
1709
  function fieldsOf(value, path = []) {
1468
1710
  if (typeof value === "string") return value.trim() === "" ? [] : [{
@@ -1583,8 +1825,7 @@ function checkAiCritique(raw, request) {
1583
1825
  }
1584
1826
  };
1585
1827
  }
1586
- //#endregion
1587
- //#region src/ai-drafts.ts
1828
+ // src/ai-drafts.ts
1588
1829
  /** The most drafts one request may ask for, and the most a reply may hold when it names no count. */
1589
1830
  const AI_DRAFTS_MAX_COUNT = 50;
1590
1831
  /** The most repair passes `generateDrafts` makes. */
@@ -1676,7 +1917,7 @@ const FIELDS = {
1676
1917
  },
1677
1918
  transcript: text("What the learner hears and types: words from the source, as they are said there."),
1678
1919
  referenceText: text("What the learner reads aloud: a sentence or two from the source."),
1679
- instructions: text("Optional. How to read it, shown to the learner."),
1920
+ instructions: text("Optional. The task, shown to the learner before the item: what to do (\"Complete the sentence. Use ONE word.\"), or how to read it aloud. Never part of the text the learner answers."),
1680
1921
  prompt: text("The writing task, as plain text."),
1681
1922
  minWords: {
1682
1923
  type: "integer",
@@ -1708,6 +1949,7 @@ const FIELDS = {
1708
1949
  const WRITES = {
1709
1950
  "multiple-choice": [
1710
1951
  "title",
1952
+ "instructions",
1711
1953
  "question",
1712
1954
  "mode",
1713
1955
  "options",
@@ -1715,12 +1957,14 @@ const WRITES = {
1715
1957
  ],
1716
1958
  "fill-in-the-blanks": [
1717
1959
  "title",
1960
+ "instructions",
1718
1961
  "passage",
1719
1962
  "blanks",
1720
1963
  "feedback"
1721
1964
  ],
1722
1965
  "gap-select": [
1723
1966
  "title",
1967
+ "instructions",
1724
1968
  "passage",
1725
1969
  "gaps",
1726
1970
  "feedback"
@@ -1858,10 +2102,16 @@ function feedbackOf(entry) {
1858
2102
  ...incorrect !== void 0 ? { incorrect } : {}
1859
2103
  } };
1860
2104
  }
2105
+ /** The instructions a model wrote for the learner, when it wrote any. */
2106
+ function instructionsOf(entry) {
2107
+ const instructions = written(entry.instructions);
2108
+ return instructions !== void 0 ? { instructions } : {};
2109
+ }
1861
2110
  /** A type's content, read from what a model wrote. */
1862
2111
  function contentOf(type, entry, newId) {
1863
2112
  switch (type) {
1864
2113
  case "multiple-choice": return {
2114
+ ...instructionsOf(entry),
1865
2115
  question: stringOf(entry.question),
1866
2116
  ...typeof entry.mode === "string" ? { mode: entry.mode } : {},
1867
2117
  options: listOf(entry.options).map((option) => {
@@ -1877,6 +2127,7 @@ function contentOf(type, entry, newId) {
1877
2127
  ...feedbackOf(entry)
1878
2128
  };
1879
2129
  case "fill-in-the-blanks": return {
2130
+ ...instructionsOf(entry),
1880
2131
  passage: stringOf(entry.passage),
1881
2132
  blanks: listOf(entry.blanks).map((blank, index) => {
1882
2133
  const fields = isRecord$1(blank) ? blank : {};
@@ -1890,6 +2141,7 @@ function contentOf(type, entry, newId) {
1890
2141
  ...feedbackOf(entry)
1891
2142
  };
1892
2143
  case "gap-select": return {
2144
+ ...instructionsOf(entry),
1893
2145
  passage: stringOf(entry.passage),
1894
2146
  gaps: listOf(entry.gaps).map((gap, index) => {
1895
2147
  const marked = listOf(isRecord$1(gap) ? gap.choices : void 0);
@@ -1910,14 +2162,11 @@ function contentOf(type, entry, newId) {
1910
2162
  transcript: stringOf(entry.transcript),
1911
2163
  ...feedbackOf(entry)
1912
2164
  };
1913
- case "read-aloud": {
1914
- const instructions = written(entry.instructions);
1915
- return {
1916
- referenceText: stringOf(entry.referenceText),
1917
- ...instructions !== void 0 ? { instructions } : {},
1918
- ...feedbackOf(entry)
1919
- };
1920
- }
2165
+ case "read-aloud": return {
2166
+ referenceText: stringOf(entry.referenceText),
2167
+ ...instructionsOf(entry),
2168
+ ...feedbackOf(entry)
2169
+ };
1921
2170
  default: {
1922
2171
  const criteria = listOf(isRecord$1(entry.rubric) ? entry.rubric.criteria : void 0);
1923
2172
  return {
@@ -2288,8 +2537,7 @@ function interactiveVideoFromDrafts(input) {
2288
2537
  findings: critiqueItemGroupDraft(group)
2289
2538
  };
2290
2539
  }
2291
- //#endregion
2292
- //#region src/ai-written.ts
2540
+ // src/ai-written.ts
2293
2541
  /**
2294
2542
  * What the AI features about a written response share: reading a model's text
2295
2543
  * fields, and finding each correction's quote in the learner's text.
@@ -2432,8 +2680,7 @@ function checkCorrections(raw, text) {
2432
2680
  corrections
2433
2681
  };
2434
2682
  }
2435
- //#endregion
2436
- //#region src/ai-grading.ts
2683
+ // src/ai-grading.ts
2437
2684
  /**
2438
2685
  * How far a model's level probabilities may sum from 1 and still be a
2439
2686
  * distribution: rounding in a model's output, not a different judgement.
@@ -2757,8 +3004,7 @@ function checkAiGrading(raw, request) {
2757
3004
  }
2758
3005
  };
2759
3006
  }
2760
- //#endregion
2761
- //#region src/ai-writing.ts
3007
+ // src/ai-writing.ts
2762
3008
  /**
2763
3009
  * The request for feedback on a draft, or `null` when there is none to give:
2764
3010
  * not a written response, an author who switched explanations off (writing
@@ -2928,7 +3174,6 @@ function checkAiWritingFeedback(raw, request) {
2928
3174
  }
2929
3175
  };
2930
3176
  }
2931
- //#endregion
2932
- export { checkAiCoaching as A, checkAiExplanation as B, createItemGroupDraft as C, AI_COACHING_MAX_TIP_LENGTH as D, validateItemGroupDraft as E, aiExplanationRequest as F, hintRevealsAnswer as H, aiGradeOf as I, aiHintRequest as L, AI_HINT_TYPES as M, AI_TEXT_MAX_LENGTH as N, AI_COACHING_MAX_WORDS as O, aiAllowedByContent as P, aiSupports as R, createInteractiveVideoDraft as S, critiqueItemGroupDraft as T, readAiTextResult as U, checkAiHint as V, AI_MATERIAL_MAX_LENGTH as W, aiCritiqueRequest as _, checkAiGrading as a, critiqueDraft as b, AI_DRAFTS_MAX_COUNT as c, aiDraftsRequest as d, checkAiDrafts as f, AI_CRITIQUE_MAX_FINDINGS as g, AI_CRITIQUE_MAX_FIELD_LENGTH as h, aiGradingRequest as i, AI_EXPLANATION_TYPES as j, aiCoachingRequest as k, AI_DRAFTS_MAX_REPAIRS as l, interactiveVideoFromDrafts as m, checkAiWritingFeedback as n, AI_WRITING_MAX_CORRECTIONS as o, generateDrafts as p, AI_GRADING_PROBABILITY_TOLERANCE as r, AI_WRITING_MAX_FIELD_LENGTH as s, aiWritingFeedbackRequest as t, aiDraftsRepairRequest as u, checkAiCritique as v, critiqueDrafts as w, validateDraft as x, createDraft as y, buildAiFacts as z };
3177
+ export { AI_COACHING_MAX_TIP_LENGTH as A, aiHintRequest as B, createItemGroupDraft as C, listMediaRefs as D, validateItemGroupDraft as E, AI_HINT_TYPES as F, hintRevealsAnswer as G, buildAiFacts as H, AI_TEXT_MAX_LENGTH as I, readAiTextResult as K, aiAllowedByContent as L, aiCoachingRequest as M, checkAiCoaching as N, mapMediaRefs as O, AI_EXPLANATION_TYPES as P, aiExplanationRequest as R, createInteractiveVideoDraft as S, critiqueItemGroupDraft as T, checkAiExplanation as U, aiSupports as V, checkAiHint as W, aiCritiqueRequest as _, checkAiGrading as a, critiqueDraft as b, AI_DRAFTS_MAX_COUNT as c, aiDraftsRequest as d, checkAiDrafts as f, AI_CRITIQUE_MAX_FINDINGS as g, AI_CRITIQUE_MAX_FIELD_LENGTH as h, aiGradingRequest as i, AI_COACHING_MAX_WORDS as j, transcriptOf as k, AI_DRAFTS_MAX_REPAIRS as l, interactiveVideoFromDrafts as m, checkAiWritingFeedback as n, AI_WRITING_MAX_CORRECTIONS as o, generateDrafts as p, AI_MATERIAL_MAX_LENGTH as q, AI_GRADING_PROBABILITY_TOLERANCE as r, AI_WRITING_MAX_FIELD_LENGTH as s, aiWritingFeedbackRequest as t, aiDraftsRepairRequest as u, checkAiCritique as v, critiqueDrafts as w, validateDraft as x, createDraft as y, aiGradeOf as z };
2933
3178
 
2934
- //# sourceMappingURL=ai-writing-D8PdhJJz.js.map
3179
+ //# sourceMappingURL=ai-writing-B7oMCt66.js.map