@intellectif/lk-core 1.5.0 → 1.6.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 (78) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/ai-check.cjs +2 -2
  3. package/dist/ai-check.d.cts +1 -1
  4. package/dist/ai-check.d.ts +1 -1
  5. package/dist/ai-check.js +2 -2
  6. package/dist/{ai-BYryFkF9.d.ts → ai-i3jwXnpX.d.cts} +45 -4
  7. package/dist/ai-i3jwXnpX.d.cts.map +1 -0
  8. package/dist/{ai-BYryFkF9.d.cts → ai-i3jwXnpX.d.ts} +45 -4
  9. package/dist/ai-i3jwXnpX.d.ts.map +1 -0
  10. package/dist/{ai-writing-Cen7-3qs.cjs → ai-writing-C4YRVrFi.cjs} +275 -15
  11. package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
  12. package/dist/{ai-writing-D8PdhJJz.js → ai-writing-OJ5m_1EB.js} +273 -31
  13. package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
  14. package/dist/{index-D7R3Ll8R.d.cts → index--U7hHV0s.d.cts} +2 -2
  15. package/dist/{index-D7R3Ll8R.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
  16. package/dist/{index-qMbxed1B.d.cts → index-CFRgZLhH.d.cts} +41 -12
  17. package/dist/index-CFRgZLhH.d.cts.map +1 -0
  18. package/dist/{index-DUaW1Tj5.d.ts → index-DeD8EI1D.d.ts} +2 -2
  19. package/dist/{index-DUaW1Tj5.d.ts.map → index-DeD8EI1D.d.ts.map} +1 -1
  20. package/dist/{index-wMc1E_aM.d.ts → index-Dj92T76o.d.ts} +2 -2
  21. package/dist/{index-wMc1E_aM.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
  22. package/dist/{index-Bg0aeWtM.d.cts → index-Uh1u57yq.d.cts} +2 -2
  23. package/dist/{index-Bg0aeWtM.d.cts.map → index-Uh1u57yq.d.cts.map} +1 -1
  24. package/dist/{index-CxpiWedW.d.ts → index-WBHSrIUU.d.ts} +41 -12
  25. package/dist/index-WBHSrIUU.d.ts.map +1 -0
  26. package/dist/index.cjs +63 -7
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +137 -5
  29. package/dist/index.d.cts.map +1 -1
  30. package/dist/index.d.ts +137 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +58 -8
  33. package/dist/index.js.map +1 -1
  34. package/dist/{item-group-DdSUCUjM.js → item-group-BbvvH_Ob.js} +3 -2
  35. package/dist/item-group-BbvvH_Ob.js.map +1 -0
  36. package/dist/{item-group-DEB7nLID.cjs → item-group-CQRbb8Rx.cjs} +3 -2
  37. package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
  38. package/dist/{registry-DaZRpqjW.js → registry-B23o9UWh.js} +65 -18
  39. package/dist/registry-B23o9UWh.js.map +1 -0
  40. package/dist/{registry-Ce00A_OW.cjs → registry-Ih5n4PVZ.cjs} +82 -17
  41. package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
  42. package/dist/{schemas-DksueYsu.cjs → schemas-Q2dyOgc2.cjs} +77 -14
  43. package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
  44. package/dist/{schemas-BioKFf3F.js → schemas-yId-bJhD.js} +72 -15
  45. package/dist/schemas-yId-bJhD.js.map +1 -0
  46. package/dist/schemas.cjs +5 -2
  47. package/dist/schemas.d.cts +2 -2
  48. package/dist/schemas.d.ts +2 -2
  49. package/dist/schemas.js +4 -3
  50. package/dist/{scoring-BKNNTz-s.js → scoring-CLMdk8i9.js} +2 -2
  51. package/dist/{scoring-BKNNTz-s.js.map → scoring-CLMdk8i9.js.map} +1 -1
  52. package/dist/{scoring-lKQVYsxy.cjs → scoring-CaPS6q8b.cjs} +2 -2
  53. package/dist/{scoring-lKQVYsxy.cjs.map → scoring-CaPS6q8b.cjs.map} +1 -1
  54. package/dist/scoring.cjs +2 -2
  55. package/dist/scoring.d.cts +2 -2
  56. package/dist/scoring.d.ts +2 -2
  57. package/dist/scoring.js +2 -2
  58. package/dist/{xapi-NYrTlpID.cjs → xapi-BX0hvRRB.cjs} +2 -2
  59. package/dist/{xapi-NYrTlpID.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
  60. package/dist/{xapi-wR6gu4qS.js → xapi-Cdk9fTvV.js} +2 -2
  61. package/dist/{xapi-wR6gu4qS.js.map → xapi-Cdk9fTvV.js.map} +1 -1
  62. package/dist/xapi.cjs +1 -1
  63. package/dist/xapi.d.cts +1 -1
  64. package/dist/xapi.d.ts +1 -1
  65. package/dist/xapi.js +1 -1
  66. package/package.json +1 -1
  67. package/dist/ai-BYryFkF9.d.cts.map +0 -1
  68. package/dist/ai-BYryFkF9.d.ts.map +0 -1
  69. package/dist/ai-writing-Cen7-3qs.cjs.map +0 -1
  70. package/dist/ai-writing-D8PdhJJz.js.map +0 -1
  71. package/dist/index-CxpiWedW.d.ts.map +0 -1
  72. package/dist/index-qMbxed1B.d.cts.map +0 -1
  73. package/dist/item-group-DEB7nLID.cjs.map +0 -1
  74. package/dist/item-group-DdSUCUjM.js.map +0 -1
  75. package/dist/registry-Ce00A_OW.cjs.map +0 -1
  76. package/dist/registry-DaZRpqjW.js.map +0 -1
  77. package/dist/schemas-BioKFf3F.js.map +0 -1
  78. package/dist/schemas-DksueYsu.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
- const require_registry = require("./registry-Ce00A_OW.cjs");
2
- const require_scoring = require("./scoring-lKQVYsxy.cjs");
3
- const require_item_group = require("./item-group-DEB7nLID.cjs");
1
+ const require_registry = require("./registry-Ih5n4PVZ.cjs");
2
+ const require_scoring = require("./scoring-CaPS6q8b.cjs");
3
+ const require_item_group = require("./item-group-CQRbb8Rx.cjs");
4
4
  //#region src/ai-material.ts
5
5
  /**
6
6
  * What a model is told about the group a question sits in: the material the
@@ -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 } : {}
@@ -742,7 +742,7 @@ function aiCoachingRequest(input) {
742
742
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
743
743
  };
744
744
  }
745
- const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
745
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
746
746
  /** A symbol as it is compared: composed, and without the spaces around it. */
747
747
  const symbolKey = (symbol) => symbol.normalize("NFC").trim();
748
748
  /**
@@ -791,7 +791,7 @@ function checkAiCoaching(raw, request) {
791
791
  });
792
792
  const seen = /* @__PURE__ */ new Set();
793
793
  for (const entry of entries) {
794
- if (!isRecord$3(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") return {
794
+ if (!isRecord$4(entry) || typeof entry.itemId !== "string" || typeof entry.tip !== "string") return {
795
795
  ok: false,
796
796
  refusal: "malformed"
797
797
  };
@@ -839,7 +839,7 @@ function checkAiCoaching(raw, request) {
839
839
  /** A sound a model names, checked against the sounds the engine reported for the word. */
840
840
  function checkSound(raw, fact) {
841
841
  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 {
842
+ if (!isRecord$4(raw) || typeof raw.expected !== "string" || raw.heard !== void 0 && raw.heard !== null && typeof raw.heard !== "string") return {
843
843
  ok: false,
844
844
  refusal: "malformed"
845
845
  };
@@ -866,6 +866,240 @@ function checkSound(raw, fact) {
866
866
  };
867
867
  }
868
868
  //#endregion
869
+ //#region src/media-refs.ts
870
+ const MAIN_KINDS = [
871
+ "audio",
872
+ "video",
873
+ "image",
874
+ "embed"
875
+ ];
876
+ function isRecord$3(value) {
877
+ return typeof value === "object" && value !== null && !Array.isArray(value);
878
+ }
879
+ function valueAt(root, path) {
880
+ let at = root;
881
+ for (const key of path) {
882
+ if (typeof key === "number" ? !Array.isArray(at) : !isRecord$3(at)) return;
883
+ at = at[key];
884
+ }
885
+ return at;
886
+ }
887
+ /** Where `item` keeps its files: what its type declares, or its `media` alone. */
888
+ function slotsOf(item) {
889
+ const declared = typeof item.type === "string" ? require_registry.getActivityTypeDescriptor(item.type)?.mediaSlots : void 0;
890
+ if (declared === void 0) return [{ path: ["media"] }];
891
+ const slots = declared(item);
892
+ return Array.isArray(slots) ? slots : [];
893
+ }
894
+ function holderRefs(holder, path, kindOfSlot, owner) {
895
+ if (!isRecord$3(holder)) return [];
896
+ const main = typeof holder.type === "string" && MAIN_KINDS.includes(holder.type) ? holder.type : kindOfSlot;
897
+ const refs = [];
898
+ const add = (field, kind, url) => {
899
+ refs.push({
900
+ path: [...path, ...field],
901
+ kind,
902
+ url,
903
+ holder,
904
+ ...owner
905
+ });
906
+ };
907
+ if (main !== void 0) add(["url"], main, typeof holder.url === "string" ? holder.url : null);
908
+ if (typeof holder.captionsUrl === "string") add(["captionsUrl"], "captions", holder.captionsUrl);
909
+ if (Array.isArray(holder.tracks)) holder.tracks.forEach((track, index) => {
910
+ if (isRecord$3(track) && typeof track.src === "string") refs.push({
911
+ path: [
912
+ ...path,
913
+ "tracks",
914
+ index,
915
+ "src"
916
+ ],
917
+ kind: "track",
918
+ url: track.src,
919
+ holder: track,
920
+ ...owner,
921
+ ...typeof track.srclang === "string" ? { srclang: track.srclang } : {}
922
+ });
923
+ });
924
+ if (typeof holder.poster === "string") add(["poster"], "poster", holder.poster);
925
+ return refs;
926
+ }
927
+ function activityRefs(item, base) {
928
+ if (!isRecord$3(item)) return [];
929
+ const owner = typeof item.id === "string" ? { activityId: item.id } : {};
930
+ return slotsOf(item).flatMap((slot) => holderRefs(valueAt(item, slot.path), [...base, ...slot.path], slot.kind, owner));
931
+ }
932
+ /**
933
+ * Every media address in one piece of content: a loose activity or an item
934
+ * group (its stimulus first, then each item in authored order), as a
935
+ * `SequenceEntry` holds them. Drafts are read too.
936
+ *
937
+ * For each activity the walker reads the places its type keeps files: an
938
+ * item's `media`, each multiple-choice option's `media`, and dictation's and
939
+ * read-aloud's `slowMedia`. In each it lists the main file, `captionsUrl`,
940
+ * every track's `src` and `poster`. A type you register declares its own with
941
+ * `mediaSlots`; without that, its `media` is read.
942
+ *
943
+ * A main file with no address yet is listed with `url: null`, so a recording
944
+ * still to be made has a place in the list. Not read: addresses inside an
945
+ * `…Html` sidecar (the SDK does not parse HTML) and a learner's recordings,
946
+ * which are responses rather than content.
947
+ *
948
+ * ```ts
949
+ * const files = listMediaRefs(entry).filter((ref) => ref.url !== null);
950
+ * ```
951
+ */
952
+ function listMediaRefs(entry) {
953
+ if (!isRecord$3(entry)) return [];
954
+ if (entry.type !== "item-group") return activityRefs(entry, []);
955
+ const refs = [];
956
+ const stimulus = entry.stimulus;
957
+ if (isRecord$3(stimulus)) {
958
+ const owner = typeof stimulus.id === "string" ? { stimulusId: stimulus.id } : {};
959
+ refs.push(...holderRefs(stimulus.media, ["stimulus", "media"], void 0, owner));
960
+ }
961
+ if (Array.isArray(entry.items)) entry.items.forEach((item, index) => {
962
+ refs.push(...activityRefs(item, ["items", index]));
963
+ });
964
+ return refs;
965
+ }
966
+ /** A deep copy of a JSON value. Content is plain data; a cycle cannot be copied. */
967
+ function copyOf(value, seen) {
968
+ if (typeof value !== "object" || value === null) return value;
969
+ if (seen.has(value)) throw new TypeError("mapMediaRefs: the content contains a circular reference.");
970
+ seen.add(value);
971
+ try {
972
+ if (Array.isArray(value)) return value.map((element) => copyOf(element, seen));
973
+ const copy = {};
974
+ for (const [key, field] of Object.entries(value)) copy[key] = copyOf(field, seen);
975
+ return copy;
976
+ } finally {
977
+ seen.delete(value);
978
+ }
979
+ }
980
+ /**
981
+ * A copy of `entry` with each address {@link listMediaRefs} finds replaced
982
+ * by what `fn` returns for it: files moved into a new folder structure, or an
983
+ * export's file names pointed at where an import stored them. Nothing else
984
+ * changes, and `entry` is never modified: with `(ref) => ref.url`, the copy is
985
+ * deep-equal to it, with the same `canonicalJson`.
986
+ *
987
+ * For a main file with no address yet (`url: null`), a string attaches one
988
+ * and `null` leaves it empty. For an address that is there, `fn` must return
989
+ * a string: removing a file is not mapping.
990
+ *
991
+ * The copy validates as the original did when every address `fn` returns is
992
+ * one the media rule accepts: `https:`, `http:`, `data:`, `blob:`, or
993
+ * root-relative (`/…`). A bundle-relative `media/part1.mp3` is refused.
994
+ *
995
+ * @throws TypeError when `fn` returns something other than a string or
996
+ * `null`, or `null` for an address that is there, or when `entry` is circular.
997
+ */
998
+ function mapMediaRefs(entry, fn) {
999
+ const refs = listMediaRefs(entry);
1000
+ const copy = copyOf(entry, /* @__PURE__ */ new Set());
1001
+ for (const ref of refs) {
1002
+ const next = fn(ref);
1003
+ if (next === null) {
1004
+ if (ref.url !== null) throw new TypeError(`mapMediaRefs: ${ref.path.join(".")} has a file; returning null would remove it, which is not mapping.`);
1005
+ continue;
1006
+ }
1007
+ if (typeof next !== "string") throw new TypeError(`mapMediaRefs: the function must return a string or null for ${ref.path.join(".")}, not ${typeof next}.`);
1008
+ if (next === ref.url) continue;
1009
+ const holder = valueAt(copy, ref.path.slice(0, -1));
1010
+ holder[ref.path[ref.path.length - 1]] = next;
1011
+ }
1012
+ return copy;
1013
+ }
1014
+ /** A holder's own text: its transcript, else its script as "Speaker: text" lines. */
1015
+ function ownText(holder) {
1016
+ if (typeof holder.transcript === "string" && holder.transcript.trim() !== "") return holder.transcript;
1017
+ 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()}`] : []) : [];
1018
+ return turns.length > 0 ? turns.join("\n") : null;
1019
+ }
1020
+ /**
1021
+ * The author-side text of a recording: its `transcript`, or else its
1022
+ * `script` as one "Speaker: text" line per turn. `null` when it has neither.
1023
+ *
1024
+ * `holder` is a media object, a stimulus, or an activity. A stimulus or an
1025
+ * activity is read for its own text first and then for its `media`'s; a
1026
+ * stimulus keeps both at its own level. Author-only: pass it to the author's
1027
+ * requests (`aiDraftsRequest`'s `source`, a critique), never to a learner's.
1028
+ *
1029
+ * ```ts
1030
+ * const text = transcriptOf(item.media);
1031
+ * if (text !== null) aiDraftsRequest({ types: ['multiple-choice'], source: { kind: 'transcript', text } });
1032
+ * ```
1033
+ */
1034
+ function transcriptOf(holder) {
1035
+ if (!isRecord$3(holder)) return null;
1036
+ return ownText(holder) ?? (isRecord$3(holder.media) ? ownText(holder.media) : null);
1037
+ }
1038
+ //#endregion
1039
+ //#region src/authoring/recordings.ts
1040
+ const written$1 = (value) => typeof value === "string" && value.trim() !== "";
1041
+ /** The script's turns that have a speaker and words, or none. */
1042
+ function turnsOf(script) {
1043
+ return Array.isArray(script) ? script.flatMap((turn) => require_registry.isRecord(turn) && typeof turn.speaker === "string" && written$1(turn.text) ? [{
1044
+ speaker: turn.speaker,
1045
+ text: turn.text
1046
+ }] : []) : [];
1047
+ }
1048
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1049
+ /** A transcript's words, with each `Speaker:` label of the script's speakers taken out. */
1050
+ function transcriptWords(transcript, speakers) {
1051
+ const labels = [...new Set(speakers.map((speaker) => speaker.trim()).filter(written$1))];
1052
+ const unlabelled = labels.length === 0 ? transcript : transcript.replace(new RegExp(`(^|\\s)(?:${labels.map(escaped).join("|")})\\s*:`, "giu"), "$1");
1053
+ return require_registry.fold(unlabelled);
1054
+ }
1055
+ /** What a recording says, as words: its transcript, or else its script's turns. */
1056
+ function spokenWords(holder) {
1057
+ const turns = turnsOf(holder.script);
1058
+ if (written$1(holder.transcript)) return transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
1059
+ return turns.length > 0 ? require_registry.fold(turns.map((turn) => turn.text).join(" ")) : null;
1060
+ }
1061
+ const sameWords = (a, b) => a.length === b.length && a.every((word, index) => word === b[index]);
1062
+ /** One object's own rules: text on media with nothing to say, and a transcript its script contradicts. */
1063
+ function holderFindings(holder, path) {
1064
+ if (!require_registry.isRecord(holder)) return [];
1065
+ const findings = [];
1066
+ const turns = turnsOf(holder.script);
1067
+ if ((written$1(holder.transcript) || turns.length > 0) && (holder.type === "image" || holder.type === "embed")) findings.push(require_registry.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.`));
1068
+ if (written$1(holder.transcript) && turns.length > 0) {
1069
+ const fromTranscript = transcriptWords(holder.transcript, turns.map((turn) => turn.speaker));
1070
+ if (!sameWords(fromTranscript, require_registry.fold(turns.map((turn) => turn.text).join(" ")))) findings.push(require_registry.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."));
1071
+ }
1072
+ return findings;
1073
+ }
1074
+ /** The field a type's own recording reads out, for the types that have one. */
1075
+ const SPOKEN_FIELD = {
1076
+ dictation: "transcript",
1077
+ "read-aloud": "referenceText"
1078
+ };
1079
+ /**
1080
+ * An item's recording rules, at every place its type keeps media. A
1081
+ * dictation's and a read-aloud's own text is what their recording says, so a
1082
+ * recording that says other words is reported too.
1083
+ */
1084
+ function recordingFindings(item) {
1085
+ const findings = listMediaRefs(item).filter((ref) => ref.path[ref.path.length - 1] === "url").flatMap((ref) => holderFindings(ref.holder, ref.path.slice(0, -1)));
1086
+ const field = typeof item.type === "string" ? SPOKEN_FIELD[item.type] : void 0;
1087
+ const media = item.media;
1088
+ if (field === void 0 || !require_registry.isRecord(media)) return findings;
1089
+ const said = spokenWords(media);
1090
+ const accepted = [item[field], ...item.type === "dictation" && Array.isArray(item.acceptedTranscripts) ? item.acceptedTranscripts : []].filter(written$1);
1091
+ if (said !== null && accepted.length > 0 && !accepted.some((text) => sameWords(said, require_registry.fold(text)))) findings.push(require_registry.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.`));
1092
+ return findings;
1093
+ }
1094
+ /** A group's material: its own transcript against its script, and its media's rules. */
1095
+ function stimulusFindings(stimulus) {
1096
+ if (!require_registry.isRecord(stimulus)) return [];
1097
+ return [...holderFindings({
1098
+ transcript: stimulus.transcript,
1099
+ script: stimulus.script
1100
+ }, ["stimulus"]), ...holderFindings(stimulus.media, ["stimulus", "media"])];
1101
+ }
1102
+ //#endregion
869
1103
  //#region src/authoring/item-group.ts
870
1104
  /**
871
1105
  * A new, empty reading or listening testlet for an editor to start from.
@@ -963,7 +1197,11 @@ function createInteractiveVideoDraft(context) {
963
1197
  function critiqueItemGroupDraft(draft) {
964
1198
  if (!require_registry.isRecord(draft)) return [];
965
1199
  const items = Array.isArray(draft.items) ? draft.items : [];
966
- return require_registry.withValidationScope(() => [...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])), ...require_registry.keyPositionFinding(items, ["items"])]);
1200
+ return require_registry.withValidationScope(() => [
1201
+ ...stimulusFindings(draft.stimulus),
1202
+ ...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])),
1203
+ ...require_registry.keyPositionFinding(items, ["items"])
1204
+ ]);
967
1205
  }
968
1206
  /**
969
1207
  * The item critic over a question set, as `<ActivitySequence>` takes one: each
@@ -1410,9 +1648,9 @@ function normalise(found) {
1410
1648
  function critiqueDraft(type, draft) {
1411
1649
  const descriptor = require_registry.getActivityTypeDescriptor(type);
1412
1650
  if (descriptor === void 0) throw new require_registry.UnknownActivityTypeError(String(type));
1651
+ if (!require_registry.isRecord(draft)) return [];
1413
1652
  const critique = descriptor.authoring?.critique;
1414
- if (!require_registry.isRecord(draft) || critique === void 0) return [];
1415
- return require_registry.withValidationScope(() => critique(draft)).map(require_registry.normaliseFinding);
1653
+ return require_registry.withValidationScope(() => [...critique === void 0 ? [] : critique(draft), ...recordingFindings(draft)]).map(require_registry.normaliseFinding);
1416
1654
  }
1417
1655
  //#endregion
1418
1656
  //#region src/ai-critique.ts
@@ -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",
@@ -3175,12 +3417,30 @@ Object.defineProperty(exports, "interactiveVideoFromDrafts", {
3175
3417
  return interactiveVideoFromDrafts;
3176
3418
  }
3177
3419
  });
3420
+ Object.defineProperty(exports, "listMediaRefs", {
3421
+ enumerable: true,
3422
+ get: function() {
3423
+ return listMediaRefs;
3424
+ }
3425
+ });
3426
+ Object.defineProperty(exports, "mapMediaRefs", {
3427
+ enumerable: true,
3428
+ get: function() {
3429
+ return mapMediaRefs;
3430
+ }
3431
+ });
3178
3432
  Object.defineProperty(exports, "readAiTextResult", {
3179
3433
  enumerable: true,
3180
3434
  get: function() {
3181
3435
  return readAiTextResult;
3182
3436
  }
3183
3437
  });
3438
+ Object.defineProperty(exports, "transcriptOf", {
3439
+ enumerable: true,
3440
+ get: function() {
3441
+ return transcriptOf;
3442
+ }
3443
+ });
3184
3444
  Object.defineProperty(exports, "validateDraft", {
3185
3445
  enumerable: true,
3186
3446
  get: function() {
@@ -3194,4 +3454,4 @@ Object.defineProperty(exports, "validateItemGroupDraft", {
3194
3454
  }
3195
3455
  });
3196
3456
 
3197
- //# sourceMappingURL=ai-writing-Cen7-3qs.cjs.map
3457
+ //# sourceMappingURL=ai-writing-C4YRVrFi.cjs.map