@intellectif/lk-core 1.10.0 → 1.11.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 (80) hide show
  1. package/CHANGELOG.md +27 -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-B1pKVkG7.cjs → ai-writing-C7f6aKNW.cjs} +27 -4
  11. package/dist/ai-writing-C7f6aKNW.cjs.map +1 -0
  12. package/dist/{ai-writing-DLvzBiZN.js → ai-writing-Dl1bJQbL.js} +22 -5
  13. package/dist/ai-writing-Dl1bJQbL.js.map +1 -0
  14. package/dist/{index-CxkqS62X.d.ts → index-B1zEN1Ur.d.ts} +25 -3
  15. package/dist/index-B1zEN1Ur.d.ts.map +1 -0
  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-DLsUnGHt.d.cts → index-CpamCGyZ.d.cts} +25 -3
  21. package/dist/index-CpamCGyZ.d.cts.map +1 -0
  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 +763 -6
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +189 -5
  29. package/dist/index.d.cts.map +1 -1
  30. package/dist/index.d.ts +189 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +735 -7
  33. package/dist/index.js.map +1 -1
  34. package/dist/item-group-D7M3E7K2.cjs +1506 -0
  35. package/dist/item-group-D7M3E7K2.cjs.map +1 -0
  36. package/dist/item-group-uNAT5EX5.js +1338 -0
  37. package/dist/item-group-uNAT5EX5.js.map +1 -0
  38. package/dist/{registry-C9O9txy5.cjs → registry-RPfZZEqi.cjs} +28 -1
  39. package/dist/registry-RPfZZEqi.cjs.map +1 -0
  40. package/dist/{registry-Bw9grsCN.js → registry-kTqbi4Lb.js} +28 -1
  41. package/dist/registry-kTqbi4Lb.js.map +1 -0
  42. package/dist/{schemas-DcEAuAeU.cjs → schemas-C4V5wfOS.cjs} +3 -3
  43. package/dist/{schemas-DcEAuAeU.cjs.map → schemas-C4V5wfOS.cjs.map} +1 -1
  44. package/dist/{schemas-D14-tnNl.js → schemas-rDgIN4Vu.js} +3 -3
  45. package/dist/{schemas-D14-tnNl.js.map → schemas-rDgIN4Vu.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-BGKdff2i.js → scoring-DshU9i7e.js} +49 -17
  51. package/dist/scoring-DshU9i7e.js.map +1 -0
  52. package/dist/{scoring-Db1CZeAn.cjs → scoring-KpYfR0lO.cjs} +49 -17
  53. package/dist/scoring-KpYfR0lO.cjs.map +1 -0
  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-CaM7f1sD.cjs} +2 -2
  59. package/dist/{xapi-DOAqH9__.cjs.map → xapi-CaM7f1sD.cjs.map} +1 -1
  60. package/dist/{xapi-BtBm8AWZ.js → xapi-fFq9KEXG.js} +2 -2
  61. package/dist/{xapi-BtBm8AWZ.js.map → xapi-fFq9KEXG.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-B1pKVkG7.cjs.map +0 -1
  68. package/dist/ai-writing-DLvzBiZN.js.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/index-CxkqS62X.d.ts.map +0 -1
  72. package/dist/index-DLsUnGHt.d.cts.map +0 -1
  73. package/dist/item-group-C9boy5Mk.cjs +0 -513
  74. package/dist/item-group-C9boy5Mk.cjs.map +0 -1
  75. package/dist/item-group-Cqng1LPl.js +0 -453
  76. package/dist/item-group-Cqng1LPl.js.map +0 -1
  77. package/dist/registry-Bw9grsCN.js.map +0 -1
  78. package/dist/registry-C9O9txy5.cjs.map +0 -1
  79. package/dist/scoring-BGKdff2i.js.map +0 -1
  80. package/dist/scoring-Db1CZeAn.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as AI_EXPLANATION_TYPES, A as validateDraft, B as optionSet, C as interactiveVideoFromDrafts, D as checkAiCritique, E as aiCritiqueRequest, F as validateItemGroupDraft, G as textsOf, H as shownScore, I as AI_VARIANT_LENGTH_BANDS, J as transcriptOf, K as listMediaRefs, L as AI_VARIANT_NEAR_COPY_THRESHOLD, M as createItemGroupDraft, N as critiqueDrafts, O as createDraft, P as critiqueItemGroupDraft, Q as checkAiCoaching, R as keysOf, S as generateDrafts, T as AI_CRITIQUE_MAX_FINDINGS, U as structureMisses, V as sharedAnswers, W as textSimilarity, X as AI_COACHING_MAX_WORDS, Y as AI_COACHING_MAX_TIP_LENGTH, Z as aiCoachingRequest, _ as AI_DRAFTS_MAX_COUNT, a as AI_VARIANTS_MAX_REPAIRS, at as aiHintRequest, b as aiDraftsRequest, c as aiVariantsRequest, ct as checkAiExplanation, d as AI_GRADING_PROBABILITY_TOLERANCE, dt as readAiTextResult, et as AI_HINT_TYPES, f as aiGradingRequest, ft as AI_MATERIAL_MAX_LENGTH, g as AI_WRITING_MAX_FIELD_LENGTH, h as AI_WRITING_MAX_CORRECTIONS, i as AI_VARIANTS_MAX_MATERIAL_LENGTH, it as aiGradeOf, j as createInteractiveVideoDraft, k as critiqueDraft, l as checkAiVariants, lt as checkAiHint, m as checkAiGrading, n as checkAiWritingFeedback, nt as aiAllowedByContent, o as AI_VARIANT_CODE_SEVERITY, ot as aiSupports, p as aiSpeakingGradingRequest, q as mapMediaRefs, r as AI_VARIANTS_MAX_EXEMPLARS, rt as aiExplanationRequest, s as aiVariantsRepairRequest, st as buildAiFacts, t as aiWritingFeedbackRequest, tt as AI_TEXT_MAX_LENGTH, u as generateVariants, ut as hintRevealsAnswer, v as AI_DRAFTS_MAX_REPAIRS, w as AI_CRITIQUE_MAX_FIELD_LENGTH, x as checkAiDrafts, y as aiDraftsRepairRequest, z as lengthMisses } from "./ai-writing-DLvzBiZN.js";
2
- import { At as SCRIPT_MAX_TURNS, B as countWords, C as READ_ALOUD_MAX_REFERENCE_LENGTH, D as SPEECH_ASSESSMENT_MAX_WORDS, F as matchText, P as levenshteinDistance, S as READ_ALOUD_MAX_DIMENSION_WEIGHT, T as READ_ALOUD_MAX_TAKES, _t as BUILT_IN_ACTIVITY_TYPES, a as readSchema, c as dictationType, ct as dictationReferenceWords, d as multipleChoiceType, dt as DICTATION_MAX_EQUIVALENCE_LENGTH, f as readAloudType, ft as DICTATION_MAX_TEXT_LENGTH, h as defineActivityType, it as UnknownActivityTypeError, l as fillInTheBlanksType, lt as DICTATION_MAX_ACCEPTED_TRANSCRIPTS, m as guardOf, n as registerActivityType, nt as DeferredScoringError, ot as alignDictation, p as writtenResponseType, pt as DICTATION_MAX_TRANSCRIPT_LENGTH, r as registeredActivityTypes, rt as RedactedScoringError, s as MEDIA_FIELD_POLICY, t as getActivityTypeDescriptor, tt as ActivitySchemaError, u as gapSelectType, ut as DICTATION_MAX_EQUIVALENCES, w as READ_ALOUD_MAX_SECONDS, yt as isBuiltInActivityType } from "./registry-Bw9grsCN.js";
3
- import { A as roundGrade, C as canonicalJson, D as computePassThreshold, E as DEFAULT_PASS_THRESHOLD, F as validateItemScoringPolicy, M as ITEM_SCORING_MAX_RETRIES, N as resolveItemScoringPolicy, O as classifyBand, P as scoreTries, S as graderConfiguration, T as fingerprint, _ as gradeFromJudgement, a as inspectWav, b as outcomeFromGrade, c as alignReadAloud, d as composeAssessmentScore, f as inAuditSample, g as seededShuffle, h as hashSeed, i as outcomeFromUnscorable, j as DEFAULT_ITEM_SCORING_POLICY, k as gte, l as validateSpeechAssessment, m as routeGrade, n as evaluateTries, o as gradeSpeakingResponse, p as outcomeFromRoutedGrade, r as score, s as gradeReadAloud, t as evaluate, u as diffDictationChars, v as gradeFromRubric, w as contentHash, x as calibrationReport, y as hasGrade } from "./scoring-BGKdff2i.js";
4
- import { a as INTERACTIVE_VIDEO_ITEM_TYPES, c as TIMELINE_MAX_QUIZZES, i as validateItemGroup, l as TIMELINE_MAX_TITLE_LENGTH, n as RedactedItemGroupSchema, o as TIMELINE_MAX_CHAPTERS, s as TIMELINE_MAX_ITEMS, u as isInteractiveVideoItemType } from "./item-group-Cqng1LPl.js";
5
- import { a as dictationJsonSchema, c as itemGroupJsonSchema, d as readAloudJsonSchema, f as stimulusJsonSchema, i as contentJsonSchema, l as jsonSchemaFor, n as validateMedia, o as fillInTheBlanksJsonSchema, p as writtenResponseJsonSchema, r as validateOptionMedia, s as gapSelectJsonSchema, t as validateActivity, u as multipleChoiceJsonSchema } from "./schemas-D14-tnNl.js";
6
- import { a as validateXAPIStatement, i as XAPI_VERB_DISPLAY, n as xapiDefinitionFor, r as XAPIVerb, t as xAPIBuilder } from "./xapi-BtBm8AWZ.js";
1
+ import { $ as checkAiCoaching, A as validateDraft, B as lengthMisses, C as interactiveVideoFromDrafts, D as checkAiCritique, E as aiCritiqueRequest, F as critiqueItemGroupDraft, G as textSimilarity, H as sharedAnswers, I as validateItemGroupDraft, J as mapMediaRefs, K as textsOf, L as AI_VARIANT_LENGTH_BANDS, M as createItemGroupDraft, N as createReadingRouteDraft, O as createDraft, P as critiqueDrafts, Q as aiCoachingRequest, R as AI_VARIANT_NEAR_COPY_THRESHOLD, S as generateDrafts, T as AI_CRITIQUE_MAX_FINDINGS, U as shownScore, V as optionSet, W as structureMisses, X as AI_COACHING_MAX_TIP_LENGTH, Y as transcriptOf, Z as AI_COACHING_MAX_WORDS, _ as AI_DRAFTS_MAX_COUNT, a as AI_VARIANTS_MAX_REPAIRS, at as aiGradeOf, b as aiDraftsRequest, c as aiVariantsRequest, ct as buildAiFacts, d as AI_GRADING_PROBABILITY_TOLERANCE, dt as hintRevealsAnswer, et as AI_EXPLANATION_TYPES, f as aiGradingRequest, ft as readAiTextResult, g as AI_WRITING_MAX_FIELD_LENGTH, h as AI_WRITING_MAX_CORRECTIONS, i as AI_VARIANTS_MAX_MATERIAL_LENGTH, it as aiExplanationRequest, j as createInteractiveVideoDraft, k as critiqueDraft, l as checkAiVariants, lt as checkAiExplanation, m as checkAiGrading, n as checkAiWritingFeedback, nt as AI_TEXT_MAX_LENGTH, o as AI_VARIANT_CODE_SEVERITY, ot as aiHintRequest, p as aiSpeakingGradingRequest, pt as AI_MATERIAL_MAX_LENGTH, q as listMediaRefs, r as AI_VARIANTS_MAX_EXEMPLARS, rt as aiAllowedByContent, s as aiVariantsRepairRequest, st as aiSupports, t as aiWritingFeedbackRequest, tt as AI_HINT_TYPES, u as generateVariants, ut as checkAiHint, v as AI_DRAFTS_MAX_REPAIRS, w as AI_CRITIQUE_MAX_FIELD_LENGTH, x as checkAiDrafts, y as aiDraftsRepairRequest, z as keysOf } from "./ai-writing-Dl1bJQbL.js";
2
+ import { At as SCRIPT_MAX_TURNS, B as countWords, C as READ_ALOUD_MAX_REFERENCE_LENGTH, D as SPEECH_ASSESSMENT_MAX_WORDS, F as matchText, P as levenshteinDistance, S as READ_ALOUD_MAX_DIMENSION_WEIGHT, T as READ_ALOUD_MAX_TAKES, _t as BUILT_IN_ACTIVITY_TYPES, a as readSchema, c as dictationType, ct as dictationReferenceWords, d as multipleChoiceType, dt as DICTATION_MAX_EQUIVALENCE_LENGTH, f as readAloudType, ft as DICTATION_MAX_TEXT_LENGTH, h as defineActivityType, it as UnknownActivityTypeError, l as fillInTheBlanksType, lt as DICTATION_MAX_ACCEPTED_TRANSCRIPTS, m as guardOf, n as registerActivityType, nt as DeferredScoringError, ot as alignDictation, p as writtenResponseType, pt as DICTATION_MAX_TRANSCRIPT_LENGTH, r as registeredActivityTypes, rt as RedactedScoringError, s as MEDIA_FIELD_POLICY, t as getActivityTypeDescriptor, tt as ActivitySchemaError, u as gapSelectType, ut as DICTATION_MAX_EQUIVALENCES, w as READ_ALOUD_MAX_SECONDS, yt as isBuiltInActivityType } from "./registry-kTqbi4Lb.js";
3
+ import { A as roundGrade, C as canonicalJson, D as computePassThreshold, E as DEFAULT_PASS_THRESHOLD, F as validateItemScoringPolicy, M as ITEM_SCORING_MAX_RETRIES, N as resolveItemScoringPolicy, O as classifyBand, P as scoreTries, S as graderConfiguration, T as fingerprint, _ as gradeFromJudgement, a as inspectWav, b as outcomeFromGrade, c as alignReadAloud, d as composeAssessmentScore, f as inAuditSample, g as seededShuffle, h as hashSeed, i as outcomeFromUnscorable, j as DEFAULT_ITEM_SCORING_POLICY, k as gte, l as validateSpeechAssessment, m as routeGrade, n as evaluateTries, o as gradeSpeakingResponse, p as outcomeFromRoutedGrade, r as score, s as gradeReadAloud, t as evaluate, u as diffDictationChars, v as gradeFromRubric, w as contentHash, x as calibrationReport, y as hasGrade } from "./scoring-DshU9i7e.js";
4
+ import { C as questionKey, D as passageLayout, E as overlaps, O as sentencesTouching, S as itemQuestions, T as countParagraphs, _ as isReadingRouteItemType, a as INTERACTIVE_VIDEO_ITEM_TYPES, b as quoteText, c as TIMELINE_MAX_QUIZZES, d as routeIssues, f as READING_ROUTE_ITEM_TYPES, g as ROUTE_WORD_LIMIT_MAX, h as ROUTE_STRATEGY_IDS, i as validateItemGroup, l as TIMELINE_MAX_TITLE_LENGTH, m as ROUTE_STRATEGIES, n as RedactedItemGroupSchema, o as TIMELINE_MAX_CHAPTERS, p as ROUTE_NOTE_MAX_LENGTH, s as TIMELINE_MAX_ITEMS, u as isInteractiveVideoItemType, v as isRouteStrategyId, w as ROUTE_SEGMENTER, x as choicesOf, y as findQuote } from "./item-group-uNAT5EX5.js";
5
+ import { a as dictationJsonSchema, c as itemGroupJsonSchema, d as readAloudJsonSchema, f as stimulusJsonSchema, i as contentJsonSchema, l as jsonSchemaFor, n as validateMedia, o as fillInTheBlanksJsonSchema, p as writtenResponseJsonSchema, r as validateOptionMedia, s as gapSelectJsonSchema, t as validateActivity, u as multipleChoiceJsonSchema } from "./schemas-rDgIN4Vu.js";
6
+ import { a as validateXAPIStatement, i as XAPI_VERB_DISPLAY, n as xapiDefinitionFor, r as XAPIVerb, t as xAPIBuilder } from "./xapi-fFq9KEXG.js";
7
7
  // src/delivery.ts
8
8
  /**
9
9
  * The policy that restricts nothing: every setting `true`. It is what an absent
@@ -771,6 +771,710 @@ function diffResponses(before, after) {
771
771
  }
772
772
  return entries;
773
773
  }
774
+ // src/reading-route/plan.ts
775
+ const isFields$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
776
+ function quoted(body, quote) {
777
+ const match = findQuote(body, quote);
778
+ return match.found ? {
779
+ ...match.range,
780
+ text: body.slice(match.range.start, match.range.end)
781
+ } : void 0;
782
+ }
783
+ const byStart = (a, b) => a.start - b.start || a.end - b.end;
784
+ function sentenceRanges(layout, indices) {
785
+ return indices.map((index) => {
786
+ const sentence = layout.sentences[index];
787
+ return {
788
+ start: sentence.start,
789
+ end: sentence.end
790
+ };
791
+ });
792
+ }
793
+ function planQuestion(body, layout, item, entry, key, number, level) {
794
+ const evidence = (Array.isArray(entry.evidence) ? entry.evidence : []).map((quote) => quoted(body, quote)).filter((range) => range !== void 0).map(({ start, end }) => ({
795
+ start,
796
+ end
797
+ })).sort(byStart);
798
+ const evidenceSentences = [...new Set(evidence.flatMap((range) => sentencesTouching(layout, range)))].sort((a, b) => a - b);
799
+ const pairs = [];
800
+ for (const keyword of entry.keywords ?? []) {
801
+ const passage = keyword.passage === void 0 ? void 0 : quoted(body, keyword.passage);
802
+ pairs.push({
803
+ index: pairs.length + 1,
804
+ question: keyword.question,
805
+ ...passage ? { passage } : {}
806
+ });
807
+ }
808
+ for (const link of entry.links ?? []) {
809
+ const from = quoted(body, link.from);
810
+ const to = quoted(body, link.to);
811
+ if (from !== void 0 && to !== void 0) pairs.push({
812
+ index: pairs.length + 1,
813
+ from,
814
+ to
815
+ });
816
+ }
817
+ const choices = choicesOf(item, entry.partId);
818
+ const traps = (entry.traps ?? []).map((trap) => {
819
+ const option = choices.find((choice) => choice.id === trap.optionId)?.text;
820
+ const lure = trap.lure === void 0 ? void 0 : quoted(body, trap.lure);
821
+ return {
822
+ optionId: trap.optionId,
823
+ kind: trap.kind,
824
+ ...option !== void 0 ? { option } : {},
825
+ ...lure !== void 0 ? { lure } : {}
826
+ };
827
+ });
828
+ return {
829
+ key,
830
+ itemId: entry.itemId,
831
+ ...entry.partId !== void 0 ? { partId: entry.partId } : {},
832
+ number,
833
+ strategy: entry.strategy,
834
+ level: evidence.length === 0 ? "solo" : level,
835
+ evidence,
836
+ evidenceMatch: entry.evidenceMatch ?? "any",
837
+ evidenceSentences,
838
+ pairs,
839
+ traps,
840
+ ...entry.wordLimit !== void 0 ? { wordLimit: entry.wordLimit } : {},
841
+ notes: isFields$1(entry.notes) ? { ...entry.notes } : {}
842
+ };
843
+ }
844
+ function step(question, move, fields = {}) {
845
+ const note = question.notes[move];
846
+ return {
847
+ id: `${question.key}/${move}`,
848
+ move,
849
+ level: question.level,
850
+ question: question.key,
851
+ number: question.number,
852
+ strategy: question.strategy,
853
+ highlight: [],
854
+ context: [],
855
+ phrases: [],
856
+ pairs: [],
857
+ traps: [],
858
+ params: {},
859
+ ...fields,
860
+ ...note !== void 0 ? { note } : {}
861
+ };
862
+ }
863
+ /** The sentences either side of `indices` within their paragraphs, not themselves. */
864
+ function neighbours(layout, indices) {
865
+ const around = /* @__PURE__ */ new Set();
866
+ for (const index of indices) {
867
+ const sentence = layout.sentences[index];
868
+ if (sentence === void 0) continue;
869
+ for (const near of [index - 1, index + 1]) if (layout.sentences[near]?.paragraph === sentence.paragraph && !indices.includes(near)) around.add(near);
870
+ }
871
+ return [...around].sort((a, b) => a - b);
872
+ }
873
+ /** Label of the paragraph or section that holds a question's first evidence. */
874
+ function regionOf(layout, question) {
875
+ const first = question.evidenceSentences[0];
876
+ if (first === void 0) return;
877
+ const paragraph = layout.paragraphs[layout.sentences[first].paragraph];
878
+ if (paragraph?.section !== void 0) return layout.sections[paragraph.section]?.label;
879
+ return paragraph?.label;
880
+ }
881
+ function stepsFor(body, layout, item, question, previous) {
882
+ const strategy = ROUTE_STRATEGIES[question.strategy];
883
+ const steps = [];
884
+ const evidenceRanges = sentenceRanges(layout, question.evidenceSentences);
885
+ const phrases = question.pairs.flatMap((pair) => pair.question === void 0 ? [] : [pair.question]);
886
+ const isChoice = item.type !== "fill-in-the-blanks";
887
+ const answerPart = item.type === "multiple-choice" ? "question" : "answer";
888
+ const runStart = previous?.strategy !== question.strategy;
889
+ const region = regionOf(layout, question);
890
+ const sameRun = previous !== void 0 && !runStart && previous.evidenceSentences.length > 0;
891
+ const lastBefore = sameRun ? previous.evidenceSentences.at(-1) : void 0;
892
+ for (const move of strategy.moves) switch (move) {
893
+ case "survey": break;
894
+ case "read-instructions": {
895
+ const hasInstructions = typeof item.instructions === "string" && item.instructions.trim() !== "";
896
+ if (runStart && (hasInstructions || question.wordLimit !== void 0)) steps.push(step(question, move, {
897
+ part: hasInstructions ? "instructions" : answerPart,
898
+ params: question.wordLimit !== void 0 ? { wordLimit: question.wordLimit } : {}
899
+ }));
900
+ break;
901
+ }
902
+ case "keywords":
903
+ steps.push(step(question, move, {
904
+ part: "question",
905
+ phrases
906
+ }));
907
+ break;
908
+ case "predict":
909
+ steps.push(step(question, move, { part: answerPart }));
910
+ break;
911
+ case "locate": {
912
+ let zone;
913
+ if (strategy.followsOrder && lastBefore !== void 0) zone = {
914
+ start: layout.sentences[lastBefore].end,
915
+ end: body.length
916
+ };
917
+ else if (question.strategy === "matching-headings" && question.evidenceSentences.length > 0) {
918
+ const paragraph = layout.paragraphs[layout.sentences[question.evidenceSentences[0]].paragraph];
919
+ if (paragraph !== void 0) zone = {
920
+ start: paragraph.start,
921
+ end: paragraph.end
922
+ };
923
+ }
924
+ steps.push(step(question, move, {
925
+ ask: "locate",
926
+ highlight: evidenceRanges,
927
+ phrases,
928
+ ...zone !== void 0 ? { zone } : {},
929
+ params: {
930
+ ...sameRun ? { previous: previous.number } : {},
931
+ ...region !== void 0 ? { region } : {}
932
+ }
933
+ }));
934
+ break;
935
+ }
936
+ case "read-around":
937
+ steps.push(step(question, move, {
938
+ highlight: evidenceRanges,
939
+ context: sentenceRanges(layout, neighbours(layout, question.evidenceSentences))
940
+ }));
941
+ break;
942
+ case "paraphrase": {
943
+ const keywordPairs = question.pairs.filter((pair) => pair.question !== void 0);
944
+ steps.push(step(question, move, {
945
+ part: isChoice && item.type !== "multiple-choice" ? "answer" : "question",
946
+ highlight: keywordPairs.flatMap((pair) => pair.passage ? [pair.passage] : []),
947
+ context: evidenceRanges,
948
+ phrases,
949
+ pairs: keywordPairs
950
+ }));
951
+ break;
952
+ }
953
+ case "decide":
954
+ steps.push(step(question, move, {
955
+ part: "options",
956
+ highlight: question.evidence
957
+ }));
958
+ break;
959
+ case "eliminate":
960
+ steps.push(step(question, move, {
961
+ part: item.type === "multiple-choice" ? "options" : "answer",
962
+ highlight: question.evidence,
963
+ context: question.traps.flatMap((trap) => trap.lure ? [trap.lure] : []),
964
+ traps: question.traps
965
+ }));
966
+ break;
967
+ case "cohesion": {
968
+ const links = question.pairs.filter((pair) => pair.from !== void 0);
969
+ steps.push(step(question, move, {
970
+ highlight: links.flatMap((pair) => [pair.from, pair.to]),
971
+ context: evidenceRanges,
972
+ pairs: links
973
+ }));
974
+ break;
975
+ }
976
+ case "check-answer":
977
+ steps.push(step(question, move, {
978
+ part: "answer",
979
+ params: question.wordLimit !== void 0 ? { wordLimit: question.wordLimit } : {}
980
+ }));
981
+ break;
982
+ case "compare-views": steps.push(step(question, move, {
983
+ part: "options",
984
+ highlight: question.evidence
985
+ }));
986
+ }
987
+ return steps;
988
+ }
989
+ /**
990
+ * The planned route of `group`: its passage laid out, each question with its
991
+ * quotes found, and the steps of every question not at `solo`. A group with
992
+ * no route, or with a redacted one, plans every question at `solo`.
993
+ */
994
+ function planRoute(group, options = {}) {
995
+ const body = typeof group.stimulus.body === "string" ? group.stimulus.body : "";
996
+ const route = group.route;
997
+ const layout = passageLayout(body, route);
998
+ const entries = /* @__PURE__ */ new Map();
999
+ for (const entry of route?.questions ?? []) entries.set(questionKey(entry.itemId, entry.partId), entry);
1000
+ const itemsById = new Map(group.items.map((item) => [item.id, item]));
1001
+ const startAt = route?.startAt ?? 1;
1002
+ const questions = [];
1003
+ for (const found of itemQuestions(group.items)) {
1004
+ const entry = entries.get(found.key);
1005
+ if (entry === void 0 || !isRouteStrategyId(entry.strategy)) continue;
1006
+ const level = options.level ?? options.levels?.[entry.strategy] ?? "walkthrough";
1007
+ questions.push(planQuestion(body, layout, itemsById.get(found.itemId), entry, found.key, startAt + found.position, level));
1008
+ }
1009
+ const steps = [];
1010
+ const surveying = questions.filter((question) => question.level !== "solo").find((question) => ROUTE_STRATEGIES[question.strategy].moves.includes("survey"));
1011
+ if (surveying !== void 0) {
1012
+ const openings = layout.paragraphs.flatMap((paragraph) => {
1013
+ const first = layout.sentences.find((sentence) => sentence.paragraph === paragraph.index && /\p{L}/u.test(body.slice(sentence.start, sentence.end)));
1014
+ return first === void 0 ? [] : [{
1015
+ start: first.start,
1016
+ end: first.end
1017
+ }];
1018
+ });
1019
+ steps.push({
1020
+ id: "survey",
1021
+ move: "survey",
1022
+ level: surveying.level,
1023
+ strategy: surveying.strategy,
1024
+ highlight: openings,
1025
+ context: [],
1026
+ phrases: [],
1027
+ pairs: [],
1028
+ traps: [],
1029
+ params: {}
1030
+ });
1031
+ }
1032
+ let previous;
1033
+ for (const question of questions) {
1034
+ if (question.level !== "solo") steps.push(...stepsFor(body, layout, itemsById.get(question.itemId) ?? {}, question, previous));
1035
+ previous = question;
1036
+ }
1037
+ const levels = {};
1038
+ for (const question of questions) levels[question.strategy] ??= question.level;
1039
+ return {
1040
+ layout,
1041
+ questions,
1042
+ steps,
1043
+ levels
1044
+ };
1045
+ }
1046
+ // src/reading-route/progress.ts
1047
+ /**
1048
+ * What a learner did in a route, read back: which answers were right, where
1049
+ * they found — or did not find — the evidence, why each wrong answer went
1050
+ * wrong, and how much help each strategy should get next time.
1051
+ *
1052
+ * None of it is a grade. The questions grade exactly as they do without a
1053
+ * route; this is the report beside the grade, and the fading that reads it.
1054
+ */
1055
+ const LEVELS = [
1056
+ "walkthrough",
1057
+ "coached",
1058
+ "solo"
1059
+ ];
1060
+ const isLevel = (value) => LEVELS.includes(value);
1061
+ const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1062
+ const isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
1063
+ const isTime = (value) => typeof value === "number" && Number.isFinite(value);
1064
+ /** The fingerprint a trace of this passage carries. */
1065
+ function routePassageFingerprint(body) {
1066
+ return fingerprint(body);
1067
+ }
1068
+ function readLevels(value) {
1069
+ const levels = {};
1070
+ if (isFields(value)) {
1071
+ for (const [strategy, level] of Object.entries(value)) if (isRouteStrategyId(strategy) && isLevel(level)) levels[strategy] = level;
1072
+ }
1073
+ return levels;
1074
+ }
1075
+ function readEvent(value) {
1076
+ if (!isFields(value) || !isTime(value.at)) return null;
1077
+ const { at } = value;
1078
+ const question = typeof value.question === "string" ? value.question : void 0;
1079
+ switch (value.type) {
1080
+ case "started": return {
1081
+ at,
1082
+ type: "started",
1083
+ levels: readLevels(value.levels)
1084
+ };
1085
+ case "finished": return {
1086
+ at,
1087
+ type: "finished"
1088
+ };
1089
+ case "step": return typeof value.step === "string" ? {
1090
+ at,
1091
+ type: "step",
1092
+ step: value.step,
1093
+ ...question !== void 0 ? { question } : {}
1094
+ } : null;
1095
+ case "located": return question !== void 0 && isCount(value.sentence) && typeof value.hit === "boolean" ? {
1096
+ at,
1097
+ type: "located",
1098
+ question,
1099
+ sentence: value.sentence,
1100
+ hit: value.hit
1101
+ } : null;
1102
+ case "shown":
1103
+ case "answered": return question !== void 0 ? {
1104
+ at,
1105
+ type: value.type,
1106
+ question
1107
+ } : null;
1108
+ case "marked": return question !== void 0 && isCount(value.sentence) && typeof value.on === "boolean" ? {
1109
+ at,
1110
+ type: "marked",
1111
+ question,
1112
+ sentence: value.sentence,
1113
+ on: value.on
1114
+ } : null;
1115
+ default: return null;
1116
+ }
1117
+ }
1118
+ /**
1119
+ * A stored trace read back. Never throws: anything that is not a trace is
1120
+ * `null`, and an event it cannot read is left out rather than failing the rest.
1121
+ */
1122
+ function readRouteTrace(value) {
1123
+ if (!isFields(value) || value.traceVersion !== "1" || typeof value.passage !== "string") return null;
1124
+ const events = Array.isArray(value.events) ? value.events.map(readEvent).filter((event) => event !== null) : [];
1125
+ return {
1126
+ traceVersion: "1",
1127
+ passage: value.passage,
1128
+ events
1129
+ };
1130
+ }
1131
+ function answered(response, partId) {
1132
+ if (response === void 0) return false;
1133
+ switch (response.type) {
1134
+ case "multiple-choice": return Array.isArray(response.selectedOptionIds) && response.selectedOptionIds.length > 0;
1135
+ case "fill-in-the-blanks": return partId !== void 0 && (response.answers?.[partId] ?? "").trim() !== "";
1136
+ case "gap-select": return partId !== void 0 && (response.selections?.[partId] ?? "") !== "";
1137
+ default: return false;
1138
+ }
1139
+ }
1140
+ function correctIn(outcome, type, partId) {
1141
+ if (outcome === void 0 || outcome.status !== "scored" && outcome.status !== "graded") return null;
1142
+ const details = outcome.status === "scored" ? outcome.details : outcome.details;
1143
+ if (!Array.isArray(details)) return null;
1144
+ if (type === "multiple-choice") return details.every((detail) => detail.outcome === "correct" || detail.outcome === "correct-omission");
1145
+ const detail = details.find((candidate) => candidate.itemId === partId);
1146
+ return detail === void 0 ? null : detail.outcome === "correct";
1147
+ }
1148
+ /**
1149
+ * Whether each question of a routed group was answered correctly, keyed by
1150
+ * question key. Graded from `outcomes` where given — a review, or a server's
1151
+ * grades — and otherwise by `evaluate` on the full item, as the item's own
1152
+ * component would grade it. A question that cannot be graded here (a redacted
1153
+ * item and no outcome) reads as not answered.
1154
+ */
1155
+ function routeAnswers(group, responses, outcomes = {}) {
1156
+ const itemsById = new Map(group.items.map((item) => [item.id, item]));
1157
+ const graded = /* @__PURE__ */ new Map();
1158
+ const outcomeOf = (itemId) => {
1159
+ if (!graded.has(itemId)) {
1160
+ let outcome = Object.hasOwn(outcomes, itemId) ? outcomes[itemId] : void 0;
1161
+ const response = Object.hasOwn(responses, itemId) ? responses[itemId] : void 0;
1162
+ if (outcome === void 0 && response !== void 0) try {
1163
+ outcome = evaluate(itemsById.get(itemId), response);
1164
+ } catch {
1165
+ outcome = void 0;
1166
+ }
1167
+ graded.set(itemId, outcome);
1168
+ }
1169
+ return graded.get(itemId);
1170
+ };
1171
+ const answers = {};
1172
+ for (const question of itemQuestions(group.items)) {
1173
+ const response = Object.hasOwn(responses, question.itemId) ? responses[question.itemId] : void 0;
1174
+ const answer = { correct: null };
1175
+ if (response?.type === "multiple-choice" && response.selectedOptionIds?.[0] !== void 0) answer.chosen = response.selectedOptionIds[0];
1176
+ else if (response?.type === "gap-select" && question.partId !== void 0) {
1177
+ const chosen = response.selections?.[question.partId];
1178
+ if (chosen !== void 0 && chosen !== "") answer.chosen = chosen;
1179
+ } else if (response?.type === "fill-in-the-blanks" && question.partId !== void 0) {
1180
+ const written = response.answers?.[question.partId];
1181
+ if (written !== void 0 && written.trim() !== "") answer.written = written;
1182
+ }
1183
+ if (answered(response, question.partId)) answer.correct = correctIn(outcomeOf(question.itemId), question.type, question.partId);
1184
+ answers[question.key] = answer;
1185
+ }
1186
+ return answers;
1187
+ }
1188
+ /** Sentences a learner has marked for a question by the end of the trace. */
1189
+ function markedSentences(trace, question) {
1190
+ const marked = /* @__PURE__ */ new Set();
1191
+ for (const event of trace.events) if (event.type === "marked" && event.question === question) {
1192
+ if (event.on) marked.add(event.sentence);
1193
+ else marked.delete(event.sentence);
1194
+ }
1195
+ return [...marked].sort((a, b) => a - b);
1196
+ }
1197
+ /**
1198
+ * Whether marked sentences find the evidence: at least one of them holds it,
1199
+ * and no more were marked than the evidence spans plus one. Marking a whole
1200
+ * paragraph is what weaker readers do, and it is not finding the answer.
1201
+ */
1202
+ function marksFindEvidence(marked, evidence) {
1203
+ return marked.some((sentence) => evidence.includes(sentence)) && marked.length <= Math.max(2, evidence.length + 1);
1204
+ }
1205
+ /**
1206
+ * The report of one route: per question, whether the learner found the
1207
+ * evidence and with how much help, whether they answered correctly, and the
1208
+ * cause of each wrong answer; per strategy, the totals that fading reads.
1209
+ * `answers` is `routeAnswers`'s result. The order the learner looked in is
1210
+ * never part of it: there is more than one good way to read.
1211
+ */
1212
+ function summarizeRoute(group, trace, answers) {
1213
+ const started = trace.events.find((event) => event.type === "started");
1214
+ const finished = [...trace.events].reverse().find((event) => event.type === "finished");
1215
+ const plan = planRoute(group, { levels: started?.type === "started" ? started.levels : {} });
1216
+ const firstStep = /* @__PURE__ */ new Map();
1217
+ const answeredAt = /* @__PURE__ */ new Map();
1218
+ for (const event of trace.events) {
1219
+ if (event.type === "step" && event.question !== void 0 && !firstStep.has(event.question)) firstStep.set(event.question, event.at);
1220
+ if (event.type === "answered" && !answeredAt.has(event.question)) answeredAt.set(event.question, event.at);
1221
+ }
1222
+ const answerTimes = [...answeredAt.entries()].sort((a, b) => a[1] - b[1]);
1223
+ const sinceBefore = /* @__PURE__ */ new Map();
1224
+ answerTimes.forEach(([key, at], index) => {
1225
+ const before = index === 0 ? started?.at : answerTimes[index - 1]?.[1];
1226
+ if (before !== void 0) sinceBefore.set(key, Math.max(0, at - before));
1227
+ });
1228
+ const questions = plan.questions.map((question) => {
1229
+ const locates = trace.events.filter((event) => event.type === "located" && event.question === question.key);
1230
+ const shown = trace.events.some((event) => event.type === "shown" && event.question === question.key);
1231
+ let located = null;
1232
+ if (locates[0] !== void 0) located = locates[0].hit;
1233
+ else if (shown) located = false;
1234
+ else {
1235
+ const marked = markedSentences(trace, question.key);
1236
+ if (marked.length > 0) located = marksFindEvidence(marked, question.evidenceSentences);
1237
+ }
1238
+ const answer = answers[question.key];
1239
+ const correct = answer?.correct ?? null;
1240
+ let cause = null;
1241
+ let trap;
1242
+ if (correct === null) cause = "unanswered";
1243
+ else if (!correct) {
1244
+ trap = question.traps.find((candidate) => candidate.optionId === answer?.chosen)?.kind;
1245
+ if (question.wordLimit !== void 0 && answer?.written !== void 0 && countWords(answer.written) > question.wordLimit) cause = "limit";
1246
+ else if (trap !== void 0) cause = "trap";
1247
+ else if (located === false) cause = "search";
1248
+ else if (located === true) cause = "meaning";
1249
+ else cause = "unknown";
1250
+ }
1251
+ const answeredTime = answeredAt.get(question.key);
1252
+ const stepTime = firstStep.get(question.key);
1253
+ const seconds = answeredTime === void 0 ? null : stepTime !== void 0 ? Math.max(0, answeredTime - stepTime) / 1e3 : (sinceBefore.get(question.key) ?? 0) / 1e3;
1254
+ return {
1255
+ key: question.key,
1256
+ itemId: question.itemId,
1257
+ ...question.partId !== void 0 ? { partId: question.partId } : {},
1258
+ number: question.number,
1259
+ strategy: question.strategy,
1260
+ level: question.level,
1261
+ located,
1262
+ tries: locates.length,
1263
+ shown,
1264
+ correct,
1265
+ cause: correct === true ? null : cause,
1266
+ ...answer?.chosen !== void 0 ? { chosen: answer.chosen } : {},
1267
+ ...trap !== void 0 && cause === "trap" ? { trap } : {},
1268
+ seconds
1269
+ };
1270
+ });
1271
+ const strategies = [];
1272
+ for (const question of questions) {
1273
+ let entry = strategies.find((candidate) => candidate.strategy === question.strategy);
1274
+ if (entry === void 0) {
1275
+ entry = {
1276
+ strategy: question.strategy,
1277
+ level: question.level,
1278
+ questions: 0,
1279
+ correct: 0,
1280
+ located: 0,
1281
+ locatable: 0
1282
+ };
1283
+ strategies.push(entry);
1284
+ }
1285
+ entry.questions += 1;
1286
+ entry.correct += question.correct === true ? 1 : 0;
1287
+ if (question.located !== null) {
1288
+ entry.locatable += 1;
1289
+ entry.located += question.located ? 1 : 0;
1290
+ }
1291
+ }
1292
+ return {
1293
+ groupId: group.id,
1294
+ questions,
1295
+ strategies,
1296
+ correct: questions.filter((question) => question.correct === true).length,
1297
+ total: questions.length,
1298
+ seconds: started !== void 0 && finished !== void 0 ? Math.max(0, finished.at - started.at) / 1e3 : null
1299
+ };
1300
+ }
1301
+ /** A stored learner state read back. Never throws: anything unreadable is a fresh state. */
1302
+ function readRouteLearnerState(value) {
1303
+ const state = {
1304
+ stateVersion: "1",
1305
+ strategies: {},
1306
+ seen: []
1307
+ };
1308
+ if (!isFields(value) || value.stateVersion !== "1") return state;
1309
+ if (isFields(value.strategies)) {
1310
+ for (const [strategy, entry] of Object.entries(value.strategies)) if (isRouteStrategyId(strategy) && isFields(entry) && isLevel(entry.level)) state.strategies[strategy] = {
1311
+ level: entry.level,
1312
+ streak: typeof entry.streak === "number" && Number.isInteger(entry.streak) ? entry.streak : 0
1313
+ };
1314
+ }
1315
+ if (Array.isArray(value.seen)) state.seen = value.seen.filter((id) => typeof id === "string");
1316
+ return state;
1317
+ }
1318
+ /**
1319
+ * The level each strategy of `group` is presented at: the learner's, or
1320
+ * `entry` for a strategy they have not met. `level` pins one for all.
1321
+ */
1322
+ function routeLevels(group, state, options = {}) {
1323
+ const levels = {};
1324
+ for (const question of group.route?.questions ?? []) if (isRouteStrategyId(question.strategy)) levels[question.strategy] = options.level ?? state?.strategies[question.strategy]?.level ?? options.entry ?? "walkthrough";
1325
+ return levels;
1326
+ }
1327
+ const lessHelp = (level) => LEVELS[Math.min(LEVELS.indexOf(level) + 1, LEVELS.length - 1)];
1328
+ const moreHelp = (level) => LEVELS[Math.max(LEVELS.indexOf(level) - 1, 0)];
1329
+ /**
1330
+ * The learner's next state after a route. Per strategy, a question succeeds
1331
+ * when it is right, its evidence was not missed, and the learner did not ask
1332
+ * to be shown; `promoteAfter` successes in a row take one level of help away,
1333
+ * `demoteAfter` failures in a row give one back, and one passage moves a
1334
+ * strategy at most one level. A passage already in `seen` changes nothing:
1335
+ * a second attempt is memory of where the answers were.
1336
+ */
1337
+ function nextRouteLearnerState(state, summary, options = {}) {
1338
+ const current = readRouteLearnerState(state);
1339
+ if (current.seen.includes(summary.groupId)) return current;
1340
+ const promoteAfter = Math.max(1, options.promoteAfter ?? 2);
1341
+ const demoteAfter = Math.max(1, options.demoteAfter ?? 2);
1342
+ const remember = Math.max(1, options.remember ?? 500);
1343
+ const strategies = { ...current.strategies };
1344
+ for (const totals of summary.strategies) {
1345
+ const base = totals.level;
1346
+ const stored = strategies[totals.strategy];
1347
+ let streak = stored?.level === base ? stored.streak : 0;
1348
+ let level = base;
1349
+ for (const question of summary.questions) {
1350
+ if (question.strategy !== totals.strategy) continue;
1351
+ streak = question.correct === true && question.located !== false && !question.shown ? Math.max(streak, 0) + 1 : Math.min(streak, 0) - 1;
1352
+ if (streak >= promoteAfter && lessHelp(level) !== level) {
1353
+ level = lessHelp(level);
1354
+ streak = 0;
1355
+ break;
1356
+ }
1357
+ if (streak <= -demoteAfter && moreHelp(level) !== level) {
1358
+ level = moreHelp(level);
1359
+ streak = 0;
1360
+ break;
1361
+ }
1362
+ }
1363
+ strategies[totals.strategy] = {
1364
+ level,
1365
+ streak: Math.max(-demoteAfter, Math.min(promoteAfter, streak))
1366
+ };
1367
+ }
1368
+ return {
1369
+ stateVersion: "1",
1370
+ strategies,
1371
+ seen: [...current.seen, summary.groupId].slice(-remember)
1372
+ };
1373
+ }
1374
+ // src/reading-route/text.ts
1375
+ const NUMBER_WORDS = [
1376
+ "",
1377
+ "ONE",
1378
+ "TWO",
1379
+ "THREE",
1380
+ "FOUR",
1381
+ "FIVE",
1382
+ "SIX",
1383
+ "SEVEN",
1384
+ "EIGHT",
1385
+ "NINE",
1386
+ "TEN"
1387
+ ];
1388
+ /** "NO MORE THAN TWO WORDS", as the instructions of a paper write it. */
1389
+ function limitWords(limit) {
1390
+ return `${NUMBER_WORDS[limit] ?? String(limit)} WORD${limit === 1 ? "" : "S"}`;
1391
+ }
1392
+ const quote = (words) => `“${words}”`;
1393
+ function list(words) {
1394
+ const quoted = words.map(quote);
1395
+ if (quoted.length <= 1) return quoted.join("");
1396
+ return `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
1397
+ }
1398
+ /** Why a trapped option attracts, in one sentence. */
1399
+ function routeTrapText(trap) {
1400
+ const option = trap.option !== void 0 && trap.option !== "" ? quote(trap.option) : "This option";
1401
+ return {
1402
+ "word-match": `${option} repeats words from the text, but the text says something different about them.`,
1403
+ opposite: `${option} says the opposite of what the text says.`,
1404
+ partial: `${option} is only partly right: the text supports one half of it, not all of it.`,
1405
+ "outside-knowledge": `${option} may be true in the real world, but this text does not say it.`,
1406
+ "too-general": `${option} is too general: the text is about something narrower.`,
1407
+ "too-specific": `${option} is too specific: it matches one detail, not the whole idea.`,
1408
+ "not-the-question": `${option} is true, but it does not answer this question.`
1409
+ }[trap.kind];
1410
+ }
1411
+ /**
1412
+ * What the learner reads at a step, in English. An author's note for the step
1413
+ * replaces the strategy's words.
1414
+ */
1415
+ function routeStepText(step) {
1416
+ if (step.note !== void 0) return step.note;
1417
+ const n = step.number ?? 0;
1418
+ const strategy = step.strategy;
1419
+ const phrases = step.phrases;
1420
+ const limit = step.params.wordLimit;
1421
+ switch (step.move) {
1422
+ case "survey":
1423
+ 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.";
1424
+ 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.";
1425
+ 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.";
1426
+ case "read-instructions":
1427
+ 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.`;
1428
+ 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?";
1429
+ if (strategy === "ynng") return "Read the instructions. Yes / No / Not Given asks about the writer's views and claims, not about facts in general.";
1430
+ return "Read the instructions first: they say exactly what to find and how to answer.";
1431
+ case "keywords":
1432
+ 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)}` : ""}.`;
1433
+ 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.`;
1434
+ 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.`;
1435
+ case "predict":
1436
+ 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.`;
1437
+ 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.`;
1438
+ 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?`;
1439
+ case "locate": {
1440
+ const target = phrases.length > 0 ? list(phrases) : "the key words";
1441
+ 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.`;
1442
+ if (strategy === "gapped-text") return `Read the sentences just before and just after gap ${n}. Which sentence in the missing paragraphs connects to them?`;
1443
+ if (strategy === "cross-text") return `Find where the texts discuss ${target}. You need what every writer thinks, not just one of them.`;
1444
+ if (strategy !== void 0 && ROUTE_STRATEGIES[strategy].followsOrder) {
1445
+ 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}.`;
1446
+ 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.`;
1447
+ }
1448
+ 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."}`;
1449
+ }
1450
+ 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.";
1451
+ case "paraphrase": {
1452
+ const shown = step.pairs.filter((pair) => pair.question !== void 0 && pair.passage !== void 0);
1453
+ 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.`;
1454
+ return "Compare the question's words with the text's. Look for the same idea in different words, not for the same words.";
1455
+ }
1456
+ case "decide":
1457
+ 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.";
1458
+ 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.";
1459
+ case "eliminate": {
1460
+ const traps = step.traps.map(routeTrapText).join(" ");
1461
+ const tail = traps === "" ? "" : ` ${traps}`;
1462
+ 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}`;
1463
+ 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}`;
1464
+ 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}`;
1465
+ 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}`;
1466
+ }
1467
+ case "cohesion": {
1468
+ const links = step.pairs.filter((pair) => pair.from !== void 0 && pair.to !== void 0);
1469
+ 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.`;
1470
+ 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.";
1471
+ }
1472
+ case "check-answer":
1473
+ 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.";
1474
+ 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.";
1475
+ case "compare-views": return "Compare what the writers say about it. Which of them agree, and which one takes a different view?";
1476
+ }
1477
+ }
774
1478
  // src/redact.ts
775
1479
  /** Merges per-call overrides over a registered policy, one level deep. */
776
1480
  function mergePolicy(base, overrides) {
@@ -917,6 +1621,28 @@ const ITEM_GROUP_FIELD_POLICY = {
917
1621
  title: "public"
918
1622
  },
919
1623
  navigation: "public"
1624
+ },
1625
+ route: {
1626
+ routeVersion: "public",
1627
+ startAt: "public",
1628
+ paragraphLabels: "public",
1629
+ sections: {
1630
+ label: "public",
1631
+ title: "public",
1632
+ paragraphs: "public"
1633
+ },
1634
+ questions: {
1635
+ itemId: "public",
1636
+ partId: "public",
1637
+ strategy: "public",
1638
+ wordLimit: "public",
1639
+ evidence: "answer-key",
1640
+ evidenceMatch: "answer-key",
1641
+ keywords: "answer-key",
1642
+ links: "answer-key",
1643
+ traps: "answer-key",
1644
+ notes: "answer-key"
1645
+ }
920
1646
  }
921
1647
  };
922
1648
  /**
@@ -1082,6 +1808,8 @@ function readMediaProgress(value, duration) {
1082
1808
  * @throws Error when no item belongs to the entry. A mistyped key would
1083
1809
  * otherwise compose nothing into a `final` result, and a refusal must
1084
1810
  * never read as a grade.
1811
+ * @throws RangeError when `policy.rounding` is not a `RoundingPolicy`, as
1812
+ * {@link composeAssessmentScore} does.
1085
1813
  */
1086
1814
  function composeTimelineScore(entryKey, items, policy) {
1087
1815
  const own = items.filter((item) => entryKeyOf(item.slotId) === entryKey);
@@ -1354,6 +2082,6 @@ function critiqueVersions(a, b, options = {}) {
1354
2082
  }
1355
2083
  };
1356
2084
  }
1357
- export { AI_COACHING_MAX_TIP_LENGTH, AI_COACHING_MAX_WORDS, AI_CRITIQUE_MAX_FIELD_LENGTH, AI_CRITIQUE_MAX_FINDINGS, AI_DRAFTS_MAX_COUNT, AI_DRAFTS_MAX_REPAIRS, AI_EXPLANATION_TYPES, AI_GRADING_PROBABILITY_TOLERANCE, AI_HINT_TYPES, AI_MATERIAL_MAX_LENGTH, AI_TEXT_MAX_LENGTH, AI_VARIANTS_MAX_EXEMPLARS, AI_VARIANTS_MAX_MATERIAL_LENGTH, AI_VARIANTS_MAX_REPAIRS, AI_VARIANT_CODE_SEVERITY, AI_VARIANT_LENGTH_BANDS, AI_VARIANT_NEAR_COPY_THRESHOLD, AI_WRITING_MAX_CORRECTIONS, AI_WRITING_MAX_FIELD_LENGTH, ActivitySchemaError, BUILT_IN_ACTIVITY_TYPES, DEFAULT_ITEM_SCORING_POLICY, DEFAULT_PASS_THRESHOLD, DICTATION_MAX_ACCEPTED_TRANSCRIPTS, DICTATION_MAX_EQUIVALENCES, DICTATION_MAX_EQUIVALENCE_LENGTH, DICTATION_MAX_TEXT_LENGTH, DICTATION_MAX_TRANSCRIPT_LENGTH, DeferredScoringError, INTERACTIVE_VIDEO_ITEM_TYPES, ITEM_SCORING_MAX_RETRIES, OPEN_DELIVERY_POLICY, READ_ALOUD_MAX_DIMENSION_WEIGHT, READ_ALOUD_MAX_REFERENCE_LENGTH, READ_ALOUD_MAX_SECONDS, READ_ALOUD_MAX_TAKES, RedactedScoringError, SCRIPT_MAX_TURNS, SPEECH_ASSESSMENT_MAX_WORDS, TIMELINE_MAX_CHAPTERS, TIMELINE_MAX_ITEMS, TIMELINE_MAX_QUIZZES, TIMELINE_MAX_TITLE_LENGTH, UnknownActivityTypeError, VERSION_FINDING_SEVERITY, XAPIVerb, XAPI_VERB_DISPLAY, aiAllowedByContent, aiCoachingRequest, aiCritiqueRequest, aiDraftsRepairRequest, aiDraftsRequest, aiExplanationRequest, aiGradeOf, aiGradingRequest, aiHintRequest, aiSpeakingGradingRequest, aiSupports, aiVariantsRepairRequest, aiVariantsRequest, aiWritingFeedbackRequest, alignDictation, alignReadAloud, assertRedacted, assertRedactedItemGroup, buildAiFacts, calibrationReport, canonicalJson, checkAiCoaching, checkAiCritique, checkAiDrafts, checkAiExplanation, checkAiGrading, checkAiHint, checkAiVariants, checkAiWritingFeedback, classifyBand, combineDeliveryPolicies, composeAssessmentScore, composeTimelineScore, computePassThreshold, contentHash, contentJsonSchema, countWords, createDraft, createInteractiveVideoDraft, createItemGroupDraft, critiqueDraft, critiqueDrafts, critiqueItemGroupDraft, critiqueVersions, defineActivityType, dictationJsonSchema, dictationReferenceWords, dictationType, diffDictationChars, diffResponses, entryKeyOf, evaluate, evaluateTries, fillInTheBlanksJsonSchema, fillInTheBlanksType, fingerprint, flattenSequence, gapSelectJsonSchema, gapSelectType, generateDrafts, generateVariants, getActivityTypeDescriptor, gradeFromJudgement, gradeFromRubric, gradeReadAloud, gradeSpeakingResponse, graderConfiguration, gte, hasGrade, hashSeed, hintRevealsAnswer, inAuditSample, inspectWav, interactiveVideoFromDrafts, isBuiltInActivityType, isInteractiveVideoItemType, isItemGroup, itemGroupJsonSchema, jsonSchemaFor, levenshteinDistance, listMediaRefs, mapMediaRefs, matchText, multipleChoiceJsonSchema, multipleChoiceType, outcomeFromGrade, outcomeFromRoutedGrade, outcomeFromUnscorable, planAttempt, planMediaBudgets, readAiTextResult, readAloudJsonSchema, readAloudType, readMediaProgress, redact, redactItemGroup, registerActivityType, registeredActivityTypes, resolveDeliveryPolicy, resolveItemScoringPolicy, resolvePlaybackPolicy, restoreAttemptState, restoreMediaPlayLedger, roundGrade, routeGrade, score, scoreTries, scoredItemsFromPlan, seededShuffle, serializeAttemptState, serializeMediaPlayLedger, slotMediaKey, stimulusJsonSchema, stimulusMediaKey, transcriptOf, validateActivity, validateDeliveryPolicy, validateDraft, validateItemGroup, validateItemGroupDraft, validateItemScoringPolicy, validateMedia, validateOptionMedia, validateSpeechAssessment, validateXAPIStatement, verifyAttemptPlan, withoutAuthorOnly, writtenResponseJsonSchema, writtenResponseType, xAPIBuilder, xapiDefinitionFor };
2085
+ export { AI_COACHING_MAX_TIP_LENGTH, AI_COACHING_MAX_WORDS, AI_CRITIQUE_MAX_FIELD_LENGTH, AI_CRITIQUE_MAX_FINDINGS, AI_DRAFTS_MAX_COUNT, AI_DRAFTS_MAX_REPAIRS, AI_EXPLANATION_TYPES, AI_GRADING_PROBABILITY_TOLERANCE, AI_HINT_TYPES, AI_MATERIAL_MAX_LENGTH, AI_TEXT_MAX_LENGTH, AI_VARIANTS_MAX_EXEMPLARS, AI_VARIANTS_MAX_MATERIAL_LENGTH, AI_VARIANTS_MAX_REPAIRS, AI_VARIANT_CODE_SEVERITY, AI_VARIANT_LENGTH_BANDS, AI_VARIANT_NEAR_COPY_THRESHOLD, AI_WRITING_MAX_CORRECTIONS, AI_WRITING_MAX_FIELD_LENGTH, ActivitySchemaError, BUILT_IN_ACTIVITY_TYPES, DEFAULT_ITEM_SCORING_POLICY, DEFAULT_PASS_THRESHOLD, DICTATION_MAX_ACCEPTED_TRANSCRIPTS, DICTATION_MAX_EQUIVALENCES, DICTATION_MAX_EQUIVALENCE_LENGTH, DICTATION_MAX_TEXT_LENGTH, DICTATION_MAX_TRANSCRIPT_LENGTH, DeferredScoringError, INTERACTIVE_VIDEO_ITEM_TYPES, ITEM_SCORING_MAX_RETRIES, OPEN_DELIVERY_POLICY, READING_ROUTE_ITEM_TYPES, READ_ALOUD_MAX_DIMENSION_WEIGHT, READ_ALOUD_MAX_REFERENCE_LENGTH, READ_ALOUD_MAX_SECONDS, READ_ALOUD_MAX_TAKES, ROUTE_NOTE_MAX_LENGTH, ROUTE_SEGMENTER, ROUTE_STRATEGIES, ROUTE_STRATEGY_IDS, ROUTE_WORD_LIMIT_MAX, RedactedScoringError, SCRIPT_MAX_TURNS, SPEECH_ASSESSMENT_MAX_WORDS, TIMELINE_MAX_CHAPTERS, TIMELINE_MAX_ITEMS, TIMELINE_MAX_QUIZZES, TIMELINE_MAX_TITLE_LENGTH, UnknownActivityTypeError, VERSION_FINDING_SEVERITY, XAPIVerb, XAPI_VERB_DISPLAY, aiAllowedByContent, aiCoachingRequest, aiCritiqueRequest, aiDraftsRepairRequest, aiDraftsRequest, aiExplanationRequest, aiGradeOf, aiGradingRequest, aiHintRequest, aiSpeakingGradingRequest, aiSupports, aiVariantsRepairRequest, aiVariantsRequest, aiWritingFeedbackRequest, alignDictation, alignReadAloud, assertRedacted, assertRedactedItemGroup, buildAiFacts, calibrationReport, canonicalJson, checkAiCoaching, checkAiCritique, checkAiDrafts, checkAiExplanation, checkAiGrading, checkAiHint, checkAiVariants, checkAiWritingFeedback, classifyBand, combineDeliveryPolicies, composeAssessmentScore, composeTimelineScore, computePassThreshold, contentHash, contentJsonSchema, countParagraphs, countWords, createDraft, createInteractiveVideoDraft, createItemGroupDraft, createReadingRouteDraft, critiqueDraft, critiqueDrafts, critiqueItemGroupDraft, critiqueVersions, defineActivityType, dictationJsonSchema, dictationReferenceWords, dictationType, diffDictationChars, diffResponses, entryKeyOf, evaluate, evaluateTries, fillInTheBlanksJsonSchema, fillInTheBlanksType, findQuote, fingerprint, flattenSequence, gapSelectJsonSchema, gapSelectType, generateDrafts, generateVariants, getActivityTypeDescriptor, gradeFromJudgement, gradeFromRubric, gradeReadAloud, gradeSpeakingResponse, graderConfiguration, gte, hasGrade, hashSeed, hintRevealsAnswer, inAuditSample, inspectWav, interactiveVideoFromDrafts, isBuiltInActivityType, isInteractiveVideoItemType, isItemGroup, isReadingRouteItemType, isRouteStrategyId, itemGroupJsonSchema, jsonSchemaFor, levenshteinDistance, listMediaRefs, mapMediaRefs, markedSentences, marksFindEvidence, matchText, multipleChoiceJsonSchema, multipleChoiceType, nextRouteLearnerState, outcomeFromGrade, outcomeFromRoutedGrade, outcomeFromUnscorable, overlaps, passageLayout, planAttempt, planMediaBudgets, planRoute, questionKey, quoteText, readAiTextResult, readAloudJsonSchema, readAloudType, readMediaProgress, readRouteLearnerState, readRouteTrace, redact, redactItemGroup, registerActivityType, registeredActivityTypes, resolveDeliveryPolicy, resolveItemScoringPolicy, resolvePlaybackPolicy, restoreAttemptState, restoreMediaPlayLedger, roundGrade, routeAnswers, routeGrade, routeIssues, routeLevels, routePassageFingerprint, routeStepText, routeTrapText, score, scoreTries, scoredItemsFromPlan, seededShuffle, sentencesTouching, serializeAttemptState, serializeMediaPlayLedger, slotMediaKey, stimulusJsonSchema, stimulusMediaKey, summarizeRoute, transcriptOf, validateActivity, validateDeliveryPolicy, validateDraft, validateItemGroup, validateItemGroupDraft, validateItemScoringPolicy, validateMedia, validateOptionMedia, validateSpeechAssessment, validateXAPIStatement, verifyAttemptPlan, withoutAuthorOnly, writtenResponseJsonSchema, writtenResponseType, xAPIBuilder, xapiDefinitionFor };
1358
2086
 
1359
2087
  //# sourceMappingURL=index.js.map