@intellectif/lk-core 1.10.1 → 1.12.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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{ai-zhc9yhRj.d.cts → ai-Cu17iVHa.d.cts} +429 -2
  3. package/dist/ai-Cu17iVHa.d.cts.map +1 -0
  4. package/dist/{ai-zhc9yhRj.d.ts → ai-Cu17iVHa.d.ts} +429 -2
  5. package/dist/ai-Cu17iVHa.d.ts.map +1 -0
  6. package/dist/ai-check.cjs +2 -2
  7. package/dist/ai-check.d.cts +1 -1
  8. package/dist/ai-check.d.ts +1 -1
  9. package/dist/ai-check.js +2 -2
  10. package/dist/{ai-writing-BaaiuU5t.js → ai-writing-BgeeCOl7.js} +22 -5
  11. package/dist/ai-writing-BgeeCOl7.js.map +1 -0
  12. package/dist/{ai-writing-_wciTWaI.cjs → ai-writing-gO4GRscd.cjs} +27 -4
  13. package/dist/ai-writing-gO4GRscd.cjs.map +1 -0
  14. package/dist/{index-DGmcCid6.d.ts → index-B1zEN1Ur.d.ts} +2 -2
  15. package/dist/{index-DGmcCid6.d.ts.map → index-B1zEN1Ur.d.ts.map} +1 -1
  16. package/dist/{index-Byar5VVs.d.ts → index-BTZWUwOe.d.ts} +2 -2
  17. package/dist/{index-Byar5VVs.d.ts.map → index-BTZWUwOe.d.ts.map} +1 -1
  18. package/dist/{index-f4YuMgFb.d.ts → index-BfUyl6ml.d.ts} +2 -2
  19. package/dist/{index-f4YuMgFb.d.ts.map → index-BfUyl6ml.d.ts.map} +1 -1
  20. package/dist/{index-BBmID0a6.d.cts → index-CpamCGyZ.d.cts} +2 -2
  21. package/dist/{index-BBmID0a6.d.cts.map → index-CpamCGyZ.d.cts.map} +1 -1
  22. package/dist/{index-BeC8tAWf.d.cts → index-DYnacygI.d.cts} +2 -2
  23. package/dist/{index-BeC8tAWf.d.cts.map → index-DYnacygI.d.cts.map} +1 -1
  24. package/dist/{index-Ccj8oL_E.d.cts → index-DjZ1vIVg.d.cts} +2 -2
  25. package/dist/{index-Ccj8oL_E.d.cts.map → index-DjZ1vIVg.d.cts.map} +1 -1
  26. package/dist/index.cjs +762 -6
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +200 -5
  29. package/dist/index.d.cts.map +1 -1
  30. package/dist/index.d.ts +200 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +733 -7
  33. package/dist/index.js.map +1 -1
  34. package/dist/item-group-CRZNJ08j.cjs +1548 -0
  35. package/dist/item-group-CRZNJ08j.cjs.map +1 -0
  36. package/dist/item-group-CnlBLM9Z.js +1374 -0
  37. package/dist/item-group-CnlBLM9Z.js.map +1 -0
  38. package/dist/{registry-Bw9grsCN.js → registry-BClnM5V3.js} +29 -1
  39. package/dist/registry-BClnM5V3.js.map +1 -0
  40. package/dist/{registry-C9O9txy5.cjs → registry-DgLcT4rZ.cjs} +29 -1
  41. package/dist/registry-DgLcT4rZ.cjs.map +1 -0
  42. package/dist/{schemas-DcEAuAeU.cjs → schemas-BA8Y8z-S.cjs} +3 -3
  43. package/dist/{schemas-DcEAuAeU.cjs.map → schemas-BA8Y8z-S.cjs.map} +1 -1
  44. package/dist/{schemas-D14-tnNl.js → schemas-VZnN68qG.js} +3 -3
  45. package/dist/{schemas-D14-tnNl.js.map → schemas-VZnN68qG.js.map} +1 -1
  46. package/dist/schemas.cjs +3 -3
  47. package/dist/schemas.d.cts +1 -1
  48. package/dist/schemas.d.ts +1 -1
  49. package/dist/schemas.js +3 -3
  50. package/dist/{scoring-hMsEMhCU.js → scoring-BxDiwN7I.js} +2 -2
  51. package/dist/{scoring-hMsEMhCU.js.map → scoring-BxDiwN7I.js.map} +1 -1
  52. package/dist/{scoring-Bmfu8RS5.cjs → scoring-Cfu_2qQ1.cjs} +2 -2
  53. package/dist/{scoring-Bmfu8RS5.cjs.map → scoring-Cfu_2qQ1.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-DOAqH9__.cjs → xapi-BhROEuZQ.cjs} +2 -2
  59. package/dist/{xapi-DOAqH9__.cjs.map → xapi-BhROEuZQ.cjs.map} +1 -1
  60. package/dist/{xapi-BtBm8AWZ.js → xapi-BitXmAYB.js} +2 -2
  61. package/dist/{xapi-BtBm8AWZ.js.map → xapi-BitXmAYB.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-writing-BaaiuU5t.js.map +0 -1
  68. package/dist/ai-writing-_wciTWaI.cjs.map +0 -1
  69. package/dist/ai-zhc9yhRj.d.cts.map +0 -1
  70. package/dist/ai-zhc9yhRj.d.ts.map +0 -1
  71. package/dist/item-group-C9boy5Mk.cjs +0 -513
  72. package/dist/item-group-C9boy5Mk.cjs.map +0 -1
  73. package/dist/item-group-Cqng1LPl.js +0 -453
  74. package/dist/item-group-Cqng1LPl.js.map +0 -1
  75. package/dist/registry-Bw9grsCN.js.map +0 -1
  76. package/dist/registry-C9O9txy5.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_registry = require("./registry-C9O9txy5.cjs");
3
- const require_ai_writing = require("./ai-writing-_wciTWaI.cjs");
4
- const require_scoring = require("./scoring-Bmfu8RS5.cjs");
5
- const require_item_group = require("./item-group-C9boy5Mk.cjs");
6
- const require_schemas = require("./schemas-DcEAuAeU.cjs");
7
- const require_xapi = require("./xapi-DOAqH9__.cjs");
2
+ const require_registry = require("./registry-DgLcT4rZ.cjs");
3
+ const require_ai_writing = require("./ai-writing-gO4GRscd.cjs");
4
+ const require_scoring = require("./scoring-Cfu_2qQ1.cjs");
5
+ const require_item_group = require("./item-group-CRZNJ08j.cjs");
6
+ const require_schemas = require("./schemas-BA8Y8z-S.cjs");
7
+ const require_xapi = require("./xapi-BhROEuZQ.cjs");
8
8
  // src/delivery.ts
9
9
  /**
10
10
  * The policy that restricts nothing: every setting `true`. It is what an absent
@@ -772,6 +772,710 @@ function diffResponses(before, after) {
772
772
  }
773
773
  return entries;
774
774
  }
775
+ // src/reading-route/plan.ts
776
+ const isFields$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
777
+ function quoted(body, quote) {
778
+ const match = require_item_group.findQuote(body, quote);
779
+ return match.found ? {
780
+ ...match.range,
781
+ text: body.slice(match.range.start, match.range.end)
782
+ } : void 0;
783
+ }
784
+ const byStart = (a, b) => a.start - b.start || a.end - b.end;
785
+ function sentenceRanges(layout, indices) {
786
+ return indices.map((index) => {
787
+ const sentence = layout.sentences[index];
788
+ return {
789
+ start: sentence.start,
790
+ end: sentence.end
791
+ };
792
+ });
793
+ }
794
+ function planQuestion(body, layout, item, entry, key, number, level) {
795
+ const evidence = (Array.isArray(entry.evidence) ? entry.evidence : []).map((quote) => quoted(body, quote)).filter((range) => range !== void 0).map(({ start, end }) => ({
796
+ start,
797
+ end
798
+ })).sort(byStart);
799
+ const evidenceSentences = [...new Set(evidence.flatMap((range) => require_item_group.sentencesTouching(layout, range)))].sort((a, b) => a - b);
800
+ const pairs = [];
801
+ for (const keyword of entry.keywords ?? []) {
802
+ const passage = keyword.passage === void 0 ? void 0 : quoted(body, keyword.passage);
803
+ pairs.push({
804
+ index: pairs.length + 1,
805
+ question: keyword.question,
806
+ ...passage ? { passage } : {}
807
+ });
808
+ }
809
+ for (const link of entry.links ?? []) {
810
+ const from = quoted(body, link.from);
811
+ const to = quoted(body, link.to);
812
+ if (from !== void 0 && to !== void 0) pairs.push({
813
+ index: pairs.length + 1,
814
+ from,
815
+ to
816
+ });
817
+ }
818
+ const choices = require_item_group.choicesOf(item, entry.partId);
819
+ const traps = (entry.traps ?? []).map((trap) => {
820
+ const option = choices.find((choice) => choice.id === trap.optionId)?.text;
821
+ const lure = trap.lure === void 0 ? void 0 : quoted(body, trap.lure);
822
+ return {
823
+ optionId: trap.optionId,
824
+ kind: trap.kind,
825
+ ...option !== void 0 ? { option } : {},
826
+ ...lure !== void 0 ? { lure } : {}
827
+ };
828
+ });
829
+ return {
830
+ key,
831
+ itemId: entry.itemId,
832
+ ...entry.partId !== void 0 ? { partId: entry.partId } : {},
833
+ number,
834
+ strategy: entry.strategy,
835
+ level: evidence.length === 0 ? "solo" : level,
836
+ evidence,
837
+ evidenceMatch: entry.evidenceMatch ?? "any",
838
+ evidenceSentences,
839
+ pairs,
840
+ traps,
841
+ ...entry.wordLimit !== void 0 ? { wordLimit: entry.wordLimit } : {},
842
+ notes: isFields$1(entry.notes) ? { ...entry.notes } : {}
843
+ };
844
+ }
845
+ function step(question, move, fields = {}) {
846
+ const note = question.notes[move];
847
+ return {
848
+ id: `${question.key}/${move}`,
849
+ move,
850
+ level: question.level,
851
+ question: question.key,
852
+ number: question.number,
853
+ strategy: question.strategy,
854
+ highlight: [],
855
+ context: [],
856
+ phrases: [],
857
+ pairs: [],
858
+ traps: [],
859
+ params: {},
860
+ ...fields,
861
+ ...note !== void 0 ? { note } : {}
862
+ };
863
+ }
864
+ /** The sentences either side of `indices` within their paragraphs, not themselves. */
865
+ function neighbours(layout, indices) {
866
+ const around = /* @__PURE__ */ new Set();
867
+ for (const index of indices) {
868
+ const sentence = layout.sentences[index];
869
+ if (sentence === void 0) continue;
870
+ for (const near of [index - 1, index + 1]) if (layout.sentences[near]?.paragraph === sentence.paragraph && !indices.includes(near)) around.add(near);
871
+ }
872
+ return [...around].sort((a, b) => a - b);
873
+ }
874
+ /** Label of the paragraph or section that holds a question's first evidence. */
875
+ function regionOf(layout, question) {
876
+ const first = question.evidenceSentences[0];
877
+ if (first === void 0) return;
878
+ const paragraph = layout.paragraphs[layout.sentences[first].paragraph];
879
+ if (paragraph?.section !== void 0) return layout.sections[paragraph.section]?.label;
880
+ return paragraph?.label;
881
+ }
882
+ function stepsFor(body, layout, item, question, previous) {
883
+ const strategy = require_item_group.ROUTE_STRATEGIES[question.strategy];
884
+ const steps = [];
885
+ const evidenceRanges = sentenceRanges(layout, question.evidenceSentences);
886
+ const phrases = question.pairs.flatMap((pair) => pair.question === void 0 ? [] : [pair.question]);
887
+ const isChoice = item.type !== "fill-in-the-blanks";
888
+ const answerPart = item.type === "multiple-choice" ? "question" : "answer";
889
+ const runStart = previous?.strategy !== question.strategy;
890
+ const region = regionOf(layout, question);
891
+ const sameRun = previous !== void 0 && !runStart && previous.evidenceSentences.length > 0;
892
+ const lastBefore = sameRun ? previous.evidenceSentences.at(-1) : void 0;
893
+ for (const move of strategy.moves) switch (move) {
894
+ case "survey": break;
895
+ case "read-instructions": {
896
+ const hasInstructions = typeof item.instructions === "string" && item.instructions.trim() !== "";
897
+ if (runStart && (hasInstructions || question.wordLimit !== void 0)) steps.push(step(question, move, {
898
+ part: hasInstructions ? "instructions" : answerPart,
899
+ params: question.wordLimit !== void 0 ? { wordLimit: question.wordLimit } : {}
900
+ }));
901
+ break;
902
+ }
903
+ case "keywords":
904
+ steps.push(step(question, move, {
905
+ part: "question",
906
+ phrases
907
+ }));
908
+ break;
909
+ case "predict":
910
+ steps.push(step(question, move, { part: answerPart }));
911
+ break;
912
+ case "locate": {
913
+ let zone;
914
+ if (strategy.followsOrder && lastBefore !== void 0) zone = {
915
+ start: layout.sentences[lastBefore].end,
916
+ end: body.length
917
+ };
918
+ else if (question.strategy === "matching-headings" && question.evidenceSentences.length > 0) {
919
+ const paragraph = layout.paragraphs[layout.sentences[question.evidenceSentences[0]].paragraph];
920
+ if (paragraph !== void 0) zone = {
921
+ start: paragraph.start,
922
+ end: paragraph.end
923
+ };
924
+ }
925
+ steps.push(step(question, move, {
926
+ ask: "locate",
927
+ highlight: evidenceRanges,
928
+ phrases,
929
+ ...zone !== void 0 ? { zone } : {},
930
+ params: {
931
+ ...sameRun ? { previous: previous.number } : {},
932
+ ...region !== void 0 ? { region } : {}
933
+ }
934
+ }));
935
+ break;
936
+ }
937
+ case "read-around":
938
+ steps.push(step(question, move, {
939
+ highlight: evidenceRanges,
940
+ context: sentenceRanges(layout, neighbours(layout, question.evidenceSentences))
941
+ }));
942
+ break;
943
+ case "paraphrase": {
944
+ const keywordPairs = question.pairs.filter((pair) => pair.question !== void 0);
945
+ steps.push(step(question, move, {
946
+ part: isChoice && item.type !== "multiple-choice" ? "answer" : "question",
947
+ highlight: keywordPairs.flatMap((pair) => pair.passage ? [pair.passage] : []),
948
+ context: evidenceRanges,
949
+ phrases,
950
+ pairs: keywordPairs
951
+ }));
952
+ break;
953
+ }
954
+ case "decide":
955
+ steps.push(step(question, move, {
956
+ part: "options",
957
+ highlight: question.evidence
958
+ }));
959
+ break;
960
+ case "eliminate":
961
+ steps.push(step(question, move, {
962
+ part: item.type === "multiple-choice" ? "options" : "answer",
963
+ highlight: question.evidence,
964
+ context: question.traps.flatMap((trap) => trap.lure ? [trap.lure] : []),
965
+ traps: question.traps
966
+ }));
967
+ break;
968
+ case "cohesion": {
969
+ const links = question.pairs.filter((pair) => pair.from !== void 0);
970
+ steps.push(step(question, move, {
971
+ highlight: links.flatMap((pair) => [pair.from, pair.to]),
972
+ context: evidenceRanges,
973
+ pairs: links
974
+ }));
975
+ break;
976
+ }
977
+ case "check-answer":
978
+ steps.push(step(question, move, {
979
+ part: "answer",
980
+ params: question.wordLimit !== void 0 ? { wordLimit: question.wordLimit } : {}
981
+ }));
982
+ break;
983
+ case "compare-views": steps.push(step(question, move, {
984
+ part: "options",
985
+ highlight: question.evidence
986
+ }));
987
+ }
988
+ return steps;
989
+ }
990
+ /**
991
+ * The planned route of `group`: its passage laid out, each question with its
992
+ * quotes found, and the steps of every question not at `solo`. A group with
993
+ * no route, or with a redacted one, plans every question at `solo`.
994
+ */
995
+ function planRoute(group, options = {}) {
996
+ const body = typeof group.stimulus.body === "string" ? group.stimulus.body : "";
997
+ const route = group.route;
998
+ const layout = require_item_group.passageLayout(body, route);
999
+ const entries = /* @__PURE__ */ new Map();
1000
+ for (const entry of route?.questions ?? []) entries.set(require_item_group.questionKey(entry.itemId, entry.partId), entry);
1001
+ const itemsById = new Map(group.items.map((item) => [item.id, item]));
1002
+ const startAt = route?.startAt ?? 1;
1003
+ const questions = [];
1004
+ for (const found of require_item_group.itemQuestions(group.items)) {
1005
+ const entry = entries.get(found.key);
1006
+ if (entry === void 0 || !require_item_group.isRouteStrategyId(entry.strategy)) continue;
1007
+ const level = options.level ?? options.levels?.[entry.strategy] ?? "walkthrough";
1008
+ questions.push(planQuestion(body, layout, itemsById.get(found.itemId), entry, found.key, startAt + found.position, level));
1009
+ }
1010
+ const steps = [];
1011
+ const surveying = questions.filter((question) => question.level !== "solo").find((question) => require_item_group.ROUTE_STRATEGIES[question.strategy].moves.includes("survey"));
1012
+ if (surveying !== void 0) {
1013
+ const openings = layout.paragraphs.flatMap((paragraph) => {
1014
+ const first = layout.sentences.find((sentence) => sentence.paragraph === paragraph.index && /\p{L}/u.test(body.slice(sentence.start, sentence.end)));
1015
+ return first === void 0 ? [] : [{
1016
+ start: first.start,
1017
+ end: first.end
1018
+ }];
1019
+ });
1020
+ steps.push({
1021
+ id: "survey",
1022
+ move: "survey",
1023
+ level: surveying.level,
1024
+ strategy: surveying.strategy,
1025
+ highlight: openings,
1026
+ context: [],
1027
+ phrases: [],
1028
+ pairs: [],
1029
+ traps: [],
1030
+ params: {}
1031
+ });
1032
+ }
1033
+ let previous;
1034
+ for (const question of questions) {
1035
+ if (question.level !== "solo") steps.push(...stepsFor(body, layout, itemsById.get(question.itemId) ?? {}, question, previous));
1036
+ previous = question;
1037
+ }
1038
+ const levels = {};
1039
+ for (const question of questions) levels[question.strategy] ??= question.level;
1040
+ return {
1041
+ layout,
1042
+ questions,
1043
+ steps,
1044
+ levels
1045
+ };
1046
+ }
1047
+ // src/reading-route/progress.ts
1048
+ /**
1049
+ * What a learner did in a route, read back: which answers were right, where
1050
+ * they found — or did not find — the evidence, why each wrong answer went
1051
+ * wrong, and how much help each strategy should get next time.
1052
+ *
1053
+ * None of it is a grade. The questions grade exactly as they do without a
1054
+ * route; this is the report beside the grade, and the fading that reads it.
1055
+ */
1056
+ const LEVELS = [
1057
+ "walkthrough",
1058
+ "coached",
1059
+ "solo"
1060
+ ];
1061
+ const isLevel = (value) => LEVELS.includes(value);
1062
+ const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1063
+ const isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
1064
+ const isTime = (value) => typeof value === "number" && Number.isFinite(value);
1065
+ /** The fingerprint a trace of this passage carries. */
1066
+ function routePassageFingerprint(body) {
1067
+ return require_scoring.fingerprint(body);
1068
+ }
1069
+ function readLevels(value) {
1070
+ const levels = {};
1071
+ if (isFields(value)) {
1072
+ for (const [strategy, level] of Object.entries(value)) if (require_item_group.isRouteStrategyId(strategy) && isLevel(level)) levels[strategy] = level;
1073
+ }
1074
+ return levels;
1075
+ }
1076
+ function readEvent(value) {
1077
+ if (!isFields(value) || !isTime(value.at)) return null;
1078
+ const { at } = value;
1079
+ const question = typeof value.question === "string" ? value.question : void 0;
1080
+ switch (value.type) {
1081
+ case "started": return {
1082
+ at,
1083
+ type: "started",
1084
+ levels: readLevels(value.levels)
1085
+ };
1086
+ case "finished": return {
1087
+ at,
1088
+ type: "finished"
1089
+ };
1090
+ case "step": return typeof value.step === "string" ? {
1091
+ at,
1092
+ type: "step",
1093
+ step: value.step,
1094
+ ...question !== void 0 ? { question } : {}
1095
+ } : null;
1096
+ case "located": return question !== void 0 && isCount(value.sentence) && typeof value.hit === "boolean" ? {
1097
+ at,
1098
+ type: "located",
1099
+ question,
1100
+ sentence: value.sentence,
1101
+ hit: value.hit
1102
+ } : null;
1103
+ case "shown":
1104
+ case "answered": return question !== void 0 ? {
1105
+ at,
1106
+ type: value.type,
1107
+ question
1108
+ } : null;
1109
+ case "marked": return question !== void 0 && isCount(value.sentence) && typeof value.on === "boolean" ? {
1110
+ at,
1111
+ type: "marked",
1112
+ question,
1113
+ sentence: value.sentence,
1114
+ on: value.on
1115
+ } : null;
1116
+ default: return null;
1117
+ }
1118
+ }
1119
+ /**
1120
+ * A stored trace read back. Never throws: anything that is not a trace is
1121
+ * `null`, and an event it cannot read is left out rather than failing the rest.
1122
+ */
1123
+ function readRouteTrace(value) {
1124
+ if (!isFields(value) || value.traceVersion !== "1" || typeof value.passage !== "string") return null;
1125
+ const events = Array.isArray(value.events) ? value.events.map(readEvent).filter((event) => event !== null) : [];
1126
+ return {
1127
+ traceVersion: "1",
1128
+ passage: value.passage,
1129
+ events
1130
+ };
1131
+ }
1132
+ function answered(response, partId) {
1133
+ if (response === void 0) return false;
1134
+ switch (response.type) {
1135
+ case "multiple-choice": return Array.isArray(response.selectedOptionIds) && response.selectedOptionIds.length > 0;
1136
+ case "fill-in-the-blanks": return partId !== void 0 && (response.answers?.[partId] ?? "").trim() !== "";
1137
+ case "gap-select": return partId !== void 0 && (response.selections?.[partId] ?? "") !== "";
1138
+ default: return false;
1139
+ }
1140
+ }
1141
+ function correctIn(outcome, type, partId) {
1142
+ if (outcome === void 0 || outcome.status !== "scored" && outcome.status !== "graded") return null;
1143
+ const details = outcome.status === "scored" ? outcome.details : outcome.details;
1144
+ if (!Array.isArray(details)) return null;
1145
+ if (type === "multiple-choice") return details.every((detail) => detail.outcome === "correct" || detail.outcome === "correct-omission");
1146
+ const detail = details.find((candidate) => candidate.itemId === partId);
1147
+ return detail === void 0 ? null : detail.outcome === "correct";
1148
+ }
1149
+ /**
1150
+ * Whether each question of a routed group was answered correctly, keyed by
1151
+ * question key. Graded from `outcomes` where given — a review, or a server's
1152
+ * grades — and otherwise by `evaluate` on the full item, as the item's own
1153
+ * component would grade it. A question that cannot be graded here (a redacted
1154
+ * item and no outcome) reads as not answered.
1155
+ */
1156
+ function routeAnswers(group, responses, outcomes = {}) {
1157
+ const itemsById = new Map(group.items.map((item) => [item.id, item]));
1158
+ const graded = /* @__PURE__ */ new Map();
1159
+ const outcomeOf = (itemId) => {
1160
+ if (!graded.has(itemId)) {
1161
+ let outcome = Object.hasOwn(outcomes, itemId) ? outcomes[itemId] : void 0;
1162
+ const response = Object.hasOwn(responses, itemId) ? responses[itemId] : void 0;
1163
+ if (outcome === void 0 && response !== void 0) try {
1164
+ outcome = require_scoring.evaluate(itemsById.get(itemId), response);
1165
+ } catch {
1166
+ outcome = void 0;
1167
+ }
1168
+ graded.set(itemId, outcome);
1169
+ }
1170
+ return graded.get(itemId);
1171
+ };
1172
+ const answers = {};
1173
+ for (const question of require_item_group.itemQuestions(group.items)) {
1174
+ const response = Object.hasOwn(responses, question.itemId) ? responses[question.itemId] : void 0;
1175
+ const answer = { correct: null };
1176
+ if (response?.type === "multiple-choice" && response.selectedOptionIds?.[0] !== void 0) answer.chosen = response.selectedOptionIds[0];
1177
+ else if (response?.type === "gap-select" && question.partId !== void 0) {
1178
+ const chosen = response.selections?.[question.partId];
1179
+ if (chosen !== void 0 && chosen !== "") answer.chosen = chosen;
1180
+ } else if (response?.type === "fill-in-the-blanks" && question.partId !== void 0) {
1181
+ const written = response.answers?.[question.partId];
1182
+ if (written !== void 0 && written.trim() !== "") answer.written = written;
1183
+ }
1184
+ if (answered(response, question.partId)) answer.correct = correctIn(outcomeOf(question.itemId), question.type, question.partId);
1185
+ answers[question.key] = answer;
1186
+ }
1187
+ return answers;
1188
+ }
1189
+ /** Sentences a learner has marked for a question by the end of the trace. */
1190
+ function markedSentences(trace, question) {
1191
+ const marked = /* @__PURE__ */ new Set();
1192
+ for (const event of trace.events) if (event.type === "marked" && event.question === question) {
1193
+ if (event.on) marked.add(event.sentence);
1194
+ else marked.delete(event.sentence);
1195
+ }
1196
+ return [...marked].sort((a, b) => a - b);
1197
+ }
1198
+ /**
1199
+ * Whether marked sentences find the evidence: at least one of them holds it,
1200
+ * and no more were marked than the evidence spans plus one. Marking a whole
1201
+ * paragraph is what weaker readers do, and it is not finding the answer.
1202
+ */
1203
+ function marksFindEvidence(marked, evidence) {
1204
+ return marked.some((sentence) => evidence.includes(sentence)) && marked.length <= Math.max(2, evidence.length + 1);
1205
+ }
1206
+ /**
1207
+ * The report of one route: per question, whether the learner found the
1208
+ * evidence and with how much help, whether they answered correctly, and the
1209
+ * cause of each wrong answer; per strategy, the totals that fading reads.
1210
+ * `answers` is `routeAnswers`'s result. The order the learner looked in is
1211
+ * never part of it: there is more than one good way to read.
1212
+ */
1213
+ function summarizeRoute(group, trace, answers) {
1214
+ const started = trace.events.find((event) => event.type === "started");
1215
+ const finished = [...trace.events].reverse().find((event) => event.type === "finished");
1216
+ const plan = planRoute(group, { levels: started?.type === "started" ? started.levels : {} });
1217
+ const firstStep = /* @__PURE__ */ new Map();
1218
+ const answeredAt = /* @__PURE__ */ new Map();
1219
+ for (const event of trace.events) {
1220
+ if (event.type === "step" && event.question !== void 0 && !firstStep.has(event.question)) firstStep.set(event.question, event.at);
1221
+ if (event.type === "answered" && !answeredAt.has(event.question)) answeredAt.set(event.question, event.at);
1222
+ }
1223
+ const answerTimes = [...answeredAt.entries()].sort((a, b) => a[1] - b[1]);
1224
+ const sinceBefore = /* @__PURE__ */ new Map();
1225
+ answerTimes.forEach(([key, at], index) => {
1226
+ const before = index === 0 ? started?.at : answerTimes[index - 1]?.[1];
1227
+ if (before !== void 0) sinceBefore.set(key, Math.max(0, at - before));
1228
+ });
1229
+ const questions = plan.questions.map((question) => {
1230
+ const locates = trace.events.filter((event) => event.type === "located" && event.question === question.key);
1231
+ const shown = trace.events.some((event) => event.type === "shown" && event.question === question.key);
1232
+ let located = null;
1233
+ if (locates[0] !== void 0) located = locates[0].hit;
1234
+ else if (shown) located = false;
1235
+ else {
1236
+ const marked = markedSentences(trace, question.key);
1237
+ if (marked.length > 0) located = marksFindEvidence(marked, question.evidenceSentences);
1238
+ }
1239
+ const answer = answers[question.key];
1240
+ const correct = answer?.correct ?? null;
1241
+ let cause = null;
1242
+ let trap;
1243
+ if (correct === null) cause = "unanswered";
1244
+ else if (!correct) {
1245
+ trap = question.traps.find((candidate) => candidate.optionId === answer?.chosen)?.kind;
1246
+ if (question.wordLimit !== void 0 && answer?.written !== void 0 && require_registry.countWords(answer.written) > question.wordLimit) cause = "limit";
1247
+ else if (trap !== void 0) cause = "trap";
1248
+ else if (located === false) cause = "search";
1249
+ else if (located === true) cause = "meaning";
1250
+ else cause = "unknown";
1251
+ }
1252
+ const answeredTime = answeredAt.get(question.key);
1253
+ const stepTime = firstStep.get(question.key);
1254
+ const seconds = answeredTime === void 0 ? null : stepTime !== void 0 ? Math.max(0, answeredTime - stepTime) / 1e3 : (sinceBefore.get(question.key) ?? 0) / 1e3;
1255
+ return {
1256
+ key: question.key,
1257
+ itemId: question.itemId,
1258
+ ...question.partId !== void 0 ? { partId: question.partId } : {},
1259
+ number: question.number,
1260
+ strategy: question.strategy,
1261
+ level: question.level,
1262
+ located,
1263
+ tries: locates.length,
1264
+ shown,
1265
+ correct,
1266
+ cause: correct === true ? null : cause,
1267
+ ...answer?.chosen !== void 0 ? { chosen: answer.chosen } : {},
1268
+ ...trap !== void 0 && cause === "trap" ? { trap } : {},
1269
+ seconds
1270
+ };
1271
+ });
1272
+ const strategies = [];
1273
+ for (const question of questions) {
1274
+ let entry = strategies.find((candidate) => candidate.strategy === question.strategy);
1275
+ if (entry === void 0) {
1276
+ entry = {
1277
+ strategy: question.strategy,
1278
+ level: question.level,
1279
+ questions: 0,
1280
+ correct: 0,
1281
+ located: 0,
1282
+ locatable: 0
1283
+ };
1284
+ strategies.push(entry);
1285
+ }
1286
+ entry.questions += 1;
1287
+ entry.correct += question.correct === true ? 1 : 0;
1288
+ if (question.located !== null) {
1289
+ entry.locatable += 1;
1290
+ entry.located += question.located ? 1 : 0;
1291
+ }
1292
+ }
1293
+ return {
1294
+ groupId: group.id,
1295
+ questions,
1296
+ strategies,
1297
+ correct: questions.filter((question) => question.correct === true).length,
1298
+ total: questions.length,
1299
+ seconds: started !== void 0 && finished !== void 0 ? Math.max(0, finished.at - started.at) / 1e3 : null
1300
+ };
1301
+ }
1302
+ /** A stored learner state read back. Never throws: anything unreadable is a fresh state. */
1303
+ function readRouteLearnerState(value) {
1304
+ const state = {
1305
+ stateVersion: "1",
1306
+ strategies: {},
1307
+ seen: []
1308
+ };
1309
+ if (!isFields(value) || value.stateVersion !== "1") return state;
1310
+ if (isFields(value.strategies)) {
1311
+ for (const [strategy, entry] of Object.entries(value.strategies)) if (require_item_group.isRouteStrategyId(strategy) && isFields(entry) && isLevel(entry.level)) state.strategies[strategy] = {
1312
+ level: entry.level,
1313
+ streak: typeof entry.streak === "number" && Number.isInteger(entry.streak) ? entry.streak : 0
1314
+ };
1315
+ }
1316
+ if (Array.isArray(value.seen)) state.seen = value.seen.filter((id) => typeof id === "string");
1317
+ return state;
1318
+ }
1319
+ /**
1320
+ * The level each strategy of `group` is presented at: the learner's, or
1321
+ * `entry` for a strategy they have not met. `level` pins one for all.
1322
+ */
1323
+ function routeLevels(group, state, options = {}) {
1324
+ const levels = {};
1325
+ for (const question of group.route?.questions ?? []) if (require_item_group.isRouteStrategyId(question.strategy)) levels[question.strategy] = options.level ?? state?.strategies[question.strategy]?.level ?? options.entry ?? "walkthrough";
1326
+ return levels;
1327
+ }
1328
+ const lessHelp = (level) => LEVELS[Math.min(LEVELS.indexOf(level) + 1, LEVELS.length - 1)];
1329
+ const moreHelp = (level) => LEVELS[Math.max(LEVELS.indexOf(level) - 1, 0)];
1330
+ /**
1331
+ * The learner's next state after a route. Per strategy, a question succeeds
1332
+ * when it is right, its evidence was not missed, and the learner did not ask
1333
+ * to be shown; `promoteAfter` successes in a row take one level of help away,
1334
+ * `demoteAfter` failures in a row give one back, and one passage moves a
1335
+ * strategy at most one level. A passage already in `seen` changes nothing:
1336
+ * a second attempt is memory of where the answers were.
1337
+ */
1338
+ function nextRouteLearnerState(state, summary, options = {}) {
1339
+ const current = readRouteLearnerState(state);
1340
+ if (current.seen.includes(summary.groupId)) return current;
1341
+ const promoteAfter = Math.max(1, options.promoteAfter ?? 2);
1342
+ const demoteAfter = Math.max(1, options.demoteAfter ?? 2);
1343
+ const remember = Math.max(1, options.remember ?? 500);
1344
+ const strategies = { ...current.strategies };
1345
+ for (const totals of summary.strategies) {
1346
+ const base = totals.level;
1347
+ const stored = strategies[totals.strategy];
1348
+ let streak = stored?.level === base ? stored.streak : 0;
1349
+ let level = base;
1350
+ for (const question of summary.questions) {
1351
+ if (question.strategy !== totals.strategy) continue;
1352
+ streak = question.correct === true && question.located !== false && !question.shown ? Math.max(streak, 0) + 1 : Math.min(streak, 0) - 1;
1353
+ if (streak >= promoteAfter && lessHelp(level) !== level) {
1354
+ level = lessHelp(level);
1355
+ streak = 0;
1356
+ break;
1357
+ }
1358
+ if (streak <= -demoteAfter && moreHelp(level) !== level) {
1359
+ level = moreHelp(level);
1360
+ streak = 0;
1361
+ break;
1362
+ }
1363
+ }
1364
+ strategies[totals.strategy] = {
1365
+ level,
1366
+ streak: Math.max(-demoteAfter, Math.min(promoteAfter, streak))
1367
+ };
1368
+ }
1369
+ return {
1370
+ stateVersion: "1",
1371
+ strategies,
1372
+ seen: [...current.seen, summary.groupId].slice(-remember)
1373
+ };
1374
+ }
1375
+ // src/reading-route/text.ts
1376
+ const NUMBER_WORDS = [
1377
+ "",
1378
+ "ONE",
1379
+ "TWO",
1380
+ "THREE",
1381
+ "FOUR",
1382
+ "FIVE",
1383
+ "SIX",
1384
+ "SEVEN",
1385
+ "EIGHT",
1386
+ "NINE",
1387
+ "TEN"
1388
+ ];
1389
+ /** "NO MORE THAN TWO WORDS", as the instructions of a paper write it. */
1390
+ function limitWords(limit) {
1391
+ return `${NUMBER_WORDS[limit] ?? String(limit)} WORD${limit === 1 ? "" : "S"}`;
1392
+ }
1393
+ const quote = (words) => `“${words}”`;
1394
+ function list(words) {
1395
+ const quoted = words.map(quote);
1396
+ if (quoted.length <= 1) return quoted.join("");
1397
+ return `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
1398
+ }
1399
+ /** Why a trapped option attracts, in one sentence. */
1400
+ function routeTrapText(trap) {
1401
+ const option = trap.option !== void 0 && trap.option !== "" ? quote(trap.option) : "This option";
1402
+ return {
1403
+ "word-match": `${option} repeats words from the text, but the text says something different about them.`,
1404
+ opposite: `${option} says the opposite of what the text says.`,
1405
+ partial: `${option} is only partly right: the text supports one half of it, not all of it.`,
1406
+ "outside-knowledge": `${option} may be true in the real world, but this text does not say it.`,
1407
+ "too-general": `${option} is too general: the text is about something narrower.`,
1408
+ "too-specific": `${option} is too specific: it matches one detail, not the whole idea.`,
1409
+ "not-the-question": `${option} is true, but it does not answer this question.`
1410
+ }[trap.kind];
1411
+ }
1412
+ /**
1413
+ * What the learner reads at a step, in English. An author's note for the step
1414
+ * replaces the strategy's words.
1415
+ */
1416
+ function routeStepText(step) {
1417
+ if (step.note !== void 0) return step.note;
1418
+ const n = step.number ?? 0;
1419
+ const strategy = step.strategy;
1420
+ const phrases = step.phrases;
1421
+ const limit = step.params.wordLimit;
1422
+ switch (step.move) {
1423
+ case "survey":
1424
+ if (strategy === "gapped-text") return "First read the main text, skipping the gaps, to follow its line of thought. Then skim the missing paragraphs to see what each is about.";
1425
+ if (strategy === "cross-text") return "Before the questions, skim each text for its writer's overall opinion. Note where each one stands on the topic.";
1426
+ return "Before the questions, skim: read the title and the first sentence of each paragraph. You want what each part is about, not the details.";
1427
+ case "read-instructions":
1428
+ if (limit !== void 0) return `Read the instructions first. Write NO MORE THAN ${limitWords(limit)} from the text: a longer answer loses the mark, even when it is correct.`;
1429
+ if (strategy === "tfng") return "Read the instructions. True / False / Not Given asks about facts: does the text give this information, contradict it, or not say?";
1430
+ if (strategy === "ynng") return "Read the instructions. Yes / No / Not Given asks about the writer's views and claims, not about facts in general.";
1431
+ return "Read the instructions first: they say exactly what to find and how to answer.";
1432
+ case "keywords":
1433
+ if (strategy === "matching-information") return `Question ${n} asks for a kind of information — an example, a reason, a comparison, a description. Note which kind${phrases.length > 0 ? `, and its key words: ${list(phrases)}` : ""}.`;
1434
+ if (phrases.length > 0) return `Find the key words in question ${n}: ${list(phrases)}. Names, numbers, dates and specialist terms are the easiest to scan for, because the text rarely changes them.`;
1435
+ return `Find the key words in question ${n}: the words that tell you what to look for. Names, numbers, dates and specialist terms are the easiest to scan for.`;
1436
+ case "predict":
1437
+ if (strategy === "multiple-choice") return `Read the question, not the options. Try to answer question ${n} in your own words before you look at the choices.`;
1438
+ if (strategy === "sentence-endings") return `Read the beginning of sentence ${n} and predict how it could end. The ending must complete it in meaning and in grammar.`;
1439
+ return `Before you look, predict the answer to question ${n}: what kind of word fits — a noun, a number, a verb, an adjective? Singular or plural?`;
1440
+ case "locate": {
1441
+ const target = phrases.length > 0 ? list(phrases) : "the key words";
1442
+ if (strategy === "matching-headings") return `Which sentence carries the main idea of ${step.params.region !== void 0 ? `paragraph ${step.params.region}` : "this paragraph"}? It is often the first or the last, but read the whole paragraph to be sure.`;
1443
+ if (strategy === "gapped-text") return `Read the sentences just before and just after gap ${n}. Which sentence in the missing paragraphs connects to them?`;
1444
+ if (strategy === "cross-text") return `Find where the texts discuss ${target}. You need what every writer thinks, not just one of them.`;
1445
+ if (strategy !== void 0 && require_item_group.ROUTE_STRATEGIES[strategy].followsOrder) {
1446
+ if (step.params.previous !== void 0) return `Scan for ${target}, or words that mean the same. The answers follow the order of the text, so start after the answer to question ${step.params.previous}.`;
1447
+ return `Scan for ${target}, or words that mean the same. The answers follow the order of the text: this is the first question of its set, so its answer comes before the others.`;
1448
+ }
1449
+ return `Scan the whole text for ${target}, or words that mean the same.${strategy === "matching-information" ? " This information can be anywhere, and one paragraph may hold more than one answer." : " These answers can come in any order."}`;
1450
+ }
1451
+ case "read-around": return "Now read this sentence carefully, with the one before and the one after it. The answer depends on every word — small ones like \"only\", \"all\", \"not\" and \"some\" most of all.";
1452
+ case "paraphrase": {
1453
+ const shown = step.pairs.filter((pair) => pair.question !== void 0 && pair.passage !== void 0);
1454
+ if (shown.length > 0) return `Same meaning, different words: ${shown.map((pair) => `the question says ${quote(pair.question)}, the text says ${quote(pair.passage?.text)}`).join("; ")}. That is how answers are hidden.`;
1455
+ return "Compare the question's words with the text's. Look for the same idea in different words, not for the same words.";
1456
+ }
1457
+ case "decide":
1458
+ if (strategy === "ynng") return "Decide. If the writer agrees with the statement, it is Yes. If the writer says the opposite, it is No. If you cannot tell what the writer thinks, it is Not Given.";
1459
+ return "Decide. If the text agrees with the statement, it is True. If it says the opposite, it is False. If it does not say, it is Not Given. Use only the text, never what you know.";
1460
+ case "eliminate": {
1461
+ const traps = step.traps.map(routeTrapText).join(" ");
1462
+ const tail = traps === "" ? "" : ` ${traps}`;
1463
+ if (strategy === "matching-headings") return `Compare the paragraph's main idea with the headings. A heading that matches a single detail or example is a trap.${tail}`;
1464
+ if (strategy === "sentence-endings") return `Check each ending: it must say what the text says and fit the grammar of the beginning. Some endings are true but belong to another sentence.${tail}`;
1465
+ if (strategy === "gapped-text") return `Test your choice: does its first sentence follow on from the text before the gap, and does its last sentence lead into the text after it? One paragraph fits nowhere.${tail}`;
1466
+ return `Check every option against the sentence you found. Reject one that repeats words from the text but changes the meaning, or that is true but does not answer the question.${tail}`;
1467
+ }
1468
+ case "cohesion": {
1469
+ const links = step.pairs.filter((pair) => pair.from !== void 0 && pair.to !== void 0);
1470
+ if (links.length > 0) return `Follow the links: ${links.map((pair) => `${quote(pair.from?.text)} points to ${quote(pair.to?.text)}`).join("; ")}. Words like these show which paragraph belongs in the gap.`;
1471
+ return "Look for words that point backwards or forwards — this, these, such, it, they; however, as a result, also. They show which paragraph belongs in the gap.";
1472
+ }
1473
+ case "check-answer":
1474
+ if (strategy === "summary-word-list") return "Check your choice fits the grammar of the summary and means what the text says. The list uses different words from the text.";
1475
+ return limit !== void 0 ? `Check your answer: spell it exactly as the text does, make sure it fits the grammar of the sentence, and count the words — no more than ${limit}.` : "Check your answer: spell it exactly as the text does, and make sure it fits the grammar of the sentence.";
1476
+ case "compare-views": return "Compare what the writers say about it. Which of them agree, and which one takes a different view?";
1477
+ }
1478
+ }
775
1479
  // src/redact.ts
776
1480
  /** Merges per-call overrides over a registered policy, one level deep. */
777
1481
  function mergePolicy(base, overrides) {
@@ -918,6 +1622,28 @@ const ITEM_GROUP_FIELD_POLICY = {
918
1622
  title: "public"
919
1623
  },
920
1624
  navigation: "public"
1625
+ },
1626
+ route: {
1627
+ routeVersion: "public",
1628
+ startAt: "public",
1629
+ paragraphLabels: "public",
1630
+ sections: {
1631
+ label: "public",
1632
+ title: "public",
1633
+ paragraphs: "public"
1634
+ },
1635
+ questions: {
1636
+ itemId: "public",
1637
+ partId: "public",
1638
+ strategy: "public",
1639
+ wordLimit: "public",
1640
+ evidence: "answer-key",
1641
+ evidenceMatch: "answer-key",
1642
+ keywords: "answer-key",
1643
+ links: "answer-key",
1644
+ traps: "answer-key",
1645
+ notes: "answer-key"
1646
+ }
921
1647
  }
922
1648
  };
923
1649
  /**
@@ -1389,10 +2115,16 @@ exports.DeferredScoringError = require_registry.DeferredScoringError;
1389
2115
  exports.INTERACTIVE_VIDEO_ITEM_TYPES = require_item_group.INTERACTIVE_VIDEO_ITEM_TYPES;
1390
2116
  exports.ITEM_SCORING_MAX_RETRIES = require_scoring.ITEM_SCORING_MAX_RETRIES;
1391
2117
  exports.OPEN_DELIVERY_POLICY = OPEN_DELIVERY_POLICY;
2118
+ exports.READING_ROUTE_ITEM_TYPES = require_item_group.READING_ROUTE_ITEM_TYPES;
1392
2119
  exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = require_registry.READ_ALOUD_MAX_DIMENSION_WEIGHT;
1393
2120
  exports.READ_ALOUD_MAX_REFERENCE_LENGTH = require_registry.READ_ALOUD_MAX_REFERENCE_LENGTH;
1394
2121
  exports.READ_ALOUD_MAX_SECONDS = require_registry.READ_ALOUD_MAX_SECONDS;
1395
2122
  exports.READ_ALOUD_MAX_TAKES = require_registry.READ_ALOUD_MAX_TAKES;
2123
+ exports.ROUTE_NOTE_MAX_LENGTH = require_item_group.ROUTE_NOTE_MAX_LENGTH;
2124
+ exports.ROUTE_SEGMENTER = require_item_group.ROUTE_SEGMENTER;
2125
+ exports.ROUTE_STRATEGIES = require_item_group.ROUTE_STRATEGIES;
2126
+ exports.ROUTE_STRATEGY_IDS = require_item_group.ROUTE_STRATEGY_IDS;
2127
+ exports.ROUTE_WORD_LIMIT_MAX = require_item_group.ROUTE_WORD_LIMIT_MAX;
1396
2128
  exports.RedactedScoringError = require_registry.RedactedScoringError;
1397
2129
  exports.SCRIPT_MAX_TURNS = require_registry.SCRIPT_MAX_TURNS;
1398
2130
  exports.SPEECH_ASSESSMENT_MAX_WORDS = require_registry.SPEECH_ASSESSMENT_MAX_WORDS;
@@ -1440,10 +2172,12 @@ exports.composeTimelineScore = composeTimelineScore;
1440
2172
  exports.computePassThreshold = require_scoring.computePassThreshold;
1441
2173
  exports.contentHash = require_scoring.contentHash;
1442
2174
  exports.contentJsonSchema = require_schemas.contentJsonSchema;
2175
+ exports.countParagraphs = require_item_group.countParagraphs;
1443
2176
  exports.countWords = require_registry.countWords;
1444
2177
  exports.createDraft = require_ai_writing.createDraft;
1445
2178
  exports.createInteractiveVideoDraft = require_ai_writing.createInteractiveVideoDraft;
1446
2179
  exports.createItemGroupDraft = require_ai_writing.createItemGroupDraft;
2180
+ exports.createReadingRouteDraft = require_ai_writing.createReadingRouteDraft;
1447
2181
  exports.critiqueDraft = require_ai_writing.critiqueDraft;
1448
2182
  exports.critiqueDrafts = require_ai_writing.critiqueDrafts;
1449
2183
  exports.critiqueItemGroupDraft = require_ai_writing.critiqueItemGroupDraft;
@@ -1459,6 +2193,7 @@ exports.evaluate = require_scoring.evaluate;
1459
2193
  exports.evaluateTries = require_scoring.evaluateTries;
1460
2194
  exports.fillInTheBlanksJsonSchema = require_schemas.fillInTheBlanksJsonSchema;
1461
2195
  exports.fillInTheBlanksType = require_registry.fillInTheBlanksType;
2196
+ exports.findQuote = require_item_group.findQuote;
1462
2197
  exports.fingerprint = require_scoring.fingerprint;
1463
2198
  exports.flattenSequence = flattenSequence;
1464
2199
  exports.gapSelectJsonSchema = require_schemas.gapSelectJsonSchema;
@@ -1481,23 +2216,36 @@ exports.interactiveVideoFromDrafts = require_ai_writing.interactiveVideoFromDraf
1481
2216
  exports.isBuiltInActivityType = require_registry.isBuiltInActivityType;
1482
2217
  exports.isInteractiveVideoItemType = require_item_group.isInteractiveVideoItemType;
1483
2218
  exports.isItemGroup = isItemGroup;
2219
+ exports.isReadingRouteItemType = require_item_group.isReadingRouteItemType;
2220
+ exports.isRouteStrategyId = require_item_group.isRouteStrategyId;
1484
2221
  exports.itemGroupJsonSchema = require_schemas.itemGroupJsonSchema;
1485
2222
  exports.jsonSchemaFor = require_schemas.jsonSchemaFor;
1486
2223
  exports.levenshteinDistance = require_registry.levenshteinDistance;
1487
2224
  exports.listMediaRefs = require_ai_writing.listMediaRefs;
1488
2225
  exports.mapMediaRefs = require_ai_writing.mapMediaRefs;
2226
+ exports.markedSentences = markedSentences;
2227
+ exports.marksFindEvidence = marksFindEvidence;
1489
2228
  exports.matchText = require_registry.matchText;
1490
2229
  exports.multipleChoiceJsonSchema = require_schemas.multipleChoiceJsonSchema;
1491
2230
  exports.multipleChoiceType = require_registry.multipleChoiceType;
2231
+ exports.nextRouteLearnerState = nextRouteLearnerState;
1492
2232
  exports.outcomeFromGrade = require_scoring.outcomeFromGrade;
1493
2233
  exports.outcomeFromRoutedGrade = require_scoring.outcomeFromRoutedGrade;
1494
2234
  exports.outcomeFromUnscorable = require_scoring.outcomeFromUnscorable;
2235
+ exports.overlaps = require_item_group.overlaps;
2236
+ exports.passageLayout = require_item_group.passageLayout;
1495
2237
  exports.planAttempt = planAttempt;
1496
2238
  exports.planMediaBudgets = planMediaBudgets;
2239
+ exports.planRoute = planRoute;
2240
+ exports.questionKey = require_item_group.questionKey;
2241
+ exports.quoteText = require_item_group.quoteText;
1497
2242
  exports.readAiTextResult = require_ai_writing.readAiTextResult;
1498
2243
  exports.readAloudJsonSchema = require_schemas.readAloudJsonSchema;
1499
2244
  exports.readAloudType = require_registry.readAloudType;
1500
2245
  exports.readMediaProgress = readMediaProgress;
2246
+ exports.readRouteLearnerState = readRouteLearnerState;
2247
+ exports.readRouteTrace = readRouteTrace;
2248
+ exports.readingQuizzes = require_item_group.readingQuizzes;
1501
2249
  exports.redact = redact;
1502
2250
  exports.redactItemGroup = redactItemGroup;
1503
2251
  exports.registerActivityType = require_registry.registerActivityType;
@@ -1508,16 +2256,24 @@ exports.resolvePlaybackPolicy = resolvePlaybackPolicy;
1508
2256
  exports.restoreAttemptState = restoreAttemptState;
1509
2257
  exports.restoreMediaPlayLedger = restoreMediaPlayLedger;
1510
2258
  exports.roundGrade = require_scoring.roundGrade;
2259
+ exports.routeAnswers = routeAnswers;
1511
2260
  exports.routeGrade = require_scoring.routeGrade;
2261
+ exports.routeIssues = require_item_group.routeIssues;
2262
+ exports.routeLevels = routeLevels;
2263
+ exports.routePassageFingerprint = routePassageFingerprint;
2264
+ exports.routeStepText = routeStepText;
2265
+ exports.routeTrapText = routeTrapText;
1512
2266
  exports.score = require_scoring.score;
1513
2267
  exports.scoreTries = require_scoring.scoreTries;
1514
2268
  exports.scoredItemsFromPlan = scoredItemsFromPlan;
1515
2269
  exports.seededShuffle = require_scoring.seededShuffle;
2270
+ exports.sentencesTouching = require_item_group.sentencesTouching;
1516
2271
  exports.serializeAttemptState = serializeAttemptState;
1517
2272
  exports.serializeMediaPlayLedger = serializeMediaPlayLedger;
1518
2273
  exports.slotMediaKey = slotMediaKey;
1519
2274
  exports.stimulusJsonSchema = require_schemas.stimulusJsonSchema;
1520
2275
  exports.stimulusMediaKey = stimulusMediaKey;
2276
+ exports.summarizeRoute = summarizeRoute;
1521
2277
  exports.transcriptOf = require_ai_writing.transcriptOf;
1522
2278
  exports.validateActivity = require_schemas.validateActivity;
1523
2279
  exports.validateDeliveryPolicy = validateDeliveryPolicy;