@intellectif/lk-core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/{ai-i3jwXnpX.d.cts → ai-B1wYrqcB.d.cts} +39 -15
  3. package/dist/ai-B1wYrqcB.d.cts.map +1 -0
  4. package/dist/{ai-i3jwXnpX.d.ts → ai-B1wYrqcB.d.ts} +39 -15
  5. package/dist/ai-B1wYrqcB.d.ts.map +1 -0
  6. package/dist/ai-check.cjs +13 -6
  7. package/dist/ai-check.cjs.map +1 -1
  8. package/dist/ai-check.d.cts +1 -5
  9. package/dist/ai-check.d.cts.map +1 -1
  10. package/dist/ai-check.d.ts +1 -5
  11. package/dist/ai-check.d.ts.map +1 -1
  12. package/dist/ai-check.js +13 -6
  13. package/dist/ai-check.js.map +1 -1
  14. package/dist/{ai-writing-OJ5m_1EB.js → ai-writing-B7oMCt66.js} +46 -43
  15. package/dist/ai-writing-B7oMCt66.js.map +1 -0
  16. package/dist/{ai-writing-C4YRVrFi.cjs → ai-writing-DrPTV3R2.cjs} +46 -43
  17. package/dist/ai-writing-DrPTV3R2.cjs.map +1 -0
  18. package/dist/{index-DeD8EI1D.d.ts → index-BM8LeDWn.d.cts} +2 -42
  19. package/dist/index-BM8LeDWn.d.cts.map +1 -0
  20. package/dist/{index-Uh1u57yq.d.cts → index-BTkMQtws.d.ts} +2 -42
  21. package/dist/index-BTkMQtws.d.ts.map +1 -0
  22. package/dist/{index-Dj92T76o.d.ts → index-BU99GFTl.d.ts} +2 -8
  23. package/dist/index-BU99GFTl.d.ts.map +1 -0
  24. package/dist/{index-WBHSrIUU.d.ts → index-BdXs4-en.d.ts} +5 -12
  25. package/dist/index-BdXs4-en.d.ts.map +1 -0
  26. package/dist/{index-CFRgZLhH.d.cts → index-DP9rvyOz.d.cts} +5 -12
  27. package/dist/index-DP9rvyOz.d.cts.map +1 -0
  28. package/dist/{index--U7hHV0s.d.cts → index-XCCoPoTU.d.cts} +2 -8
  29. package/dist/index-XCCoPoTU.d.cts.map +1 -0
  30. package/dist/index.cjs +13 -20
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +4 -70
  33. package/dist/index.d.cts.map +1 -1
  34. package/dist/index.d.ts +4 -70
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +13 -20
  37. package/dist/index.js.map +1 -1
  38. package/dist/{item-group-CQRbb8Rx.cjs → item-group-8P3WFuUK.cjs} +4 -6
  39. package/dist/{item-group-CQRbb8Rx.cjs.map → item-group-8P3WFuUK.cjs.map} +1 -1
  40. package/dist/{item-group-BbvvH_Ob.js → item-group-CeDk7mfM.js} +4 -6
  41. package/dist/{item-group-BbvvH_Ob.js.map → item-group-CeDk7mfM.js.map} +1 -1
  42. package/dist/{registry-B23o9UWh.js → registry-D92hz9iA.js} +60 -89
  43. package/dist/registry-D92hz9iA.js.map +1 -0
  44. package/dist/{registry-Ih5n4PVZ.cjs → registry-hzrkTeIT.cjs} +61 -91
  45. package/dist/registry-hzrkTeIT.cjs.map +1 -0
  46. package/dist/{schemas-Q2dyOgc2.cjs → schemas-BWeskONy.cjs} +5 -7
  47. package/dist/{schemas-Q2dyOgc2.cjs.map → schemas-BWeskONy.cjs.map} +1 -1
  48. package/dist/{schemas-yId-bJhD.js → schemas-C7TViWCg.js} +5 -7
  49. package/dist/{schemas-yId-bJhD.js.map → schemas-C7TViWCg.js.map} +1 -1
  50. package/dist/schemas.cjs +3 -3
  51. package/dist/schemas.d.cts +1 -1
  52. package/dist/schemas.d.ts +1 -1
  53. package/dist/schemas.js +3 -3
  54. package/dist/{scoring-CaPS6q8b.cjs → scoring-CtkCFyFT.cjs} +22 -42
  55. package/dist/{scoring-CaPS6q8b.cjs.map → scoring-CtkCFyFT.cjs.map} +1 -1
  56. package/dist/{scoring-CLMdk8i9.js → scoring-DD1GHcLI.js} +22 -42
  57. package/dist/{scoring-CLMdk8i9.js.map → scoring-DD1GHcLI.js.map} +1 -1
  58. package/dist/scoring.cjs +2 -2
  59. package/dist/scoring.d.cts +2 -2
  60. package/dist/scoring.d.ts +2 -2
  61. package/dist/scoring.js +2 -2
  62. package/dist/{xapi-BX0hvRRB.cjs → xapi-7btSpx6v.cjs} +5 -8
  63. package/dist/{xapi-BX0hvRRB.cjs.map → xapi-7btSpx6v.cjs.map} +1 -1
  64. package/dist/{xapi-Cdk9fTvV.js → xapi-Cgicg-zU.js} +5 -8
  65. package/dist/{xapi-Cdk9fTvV.js.map → xapi-Cgicg-zU.js.map} +1 -1
  66. package/dist/xapi.cjs +1 -1
  67. package/dist/xapi.d.cts +1 -1
  68. package/dist/xapi.d.ts +1 -1
  69. package/dist/xapi.js +1 -1
  70. package/package.json +2 -2
  71. package/dist/ai-i3jwXnpX.d.cts.map +0 -1
  72. package/dist/ai-i3jwXnpX.d.ts.map +0 -1
  73. package/dist/ai-writing-C4YRVrFi.cjs.map +0 -1
  74. package/dist/ai-writing-OJ5m_1EB.js.map +0 -1
  75. package/dist/index--U7hHV0s.d.cts.map +0 -1
  76. package/dist/index-CFRgZLhH.d.cts.map +0 -1
  77. package/dist/index-DeD8EI1D.d.ts.map +0 -1
  78. package/dist/index-Dj92T76o.d.ts.map +0 -1
  79. package/dist/index-Uh1u57yq.d.cts.map +0 -1
  80. package/dist/index-WBHSrIUU.d.ts.map +0 -1
  81. package/dist/registry-B23o9UWh.js.map +0 -1
  82. package/dist/registry-Ih5n4PVZ.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ai-check.js","names":[],"sources":["../src/ai-check/cases.ts","../src/ai-check/index.ts"],"sourcesContent":["import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport { aiCoachingRequest } from '../ai-coaching.js';\nimport { aiCritiqueRequest } from '../ai-critique.js';\nimport { aiDraftsRequest } from '../ai-drafts.js';\nimport { aiGradingRequest } from '../ai-grading.js';\nimport { aiWritingFeedbackRequest } from '../ai-writing.js';\nimport { gradeReadAloud } from '../scoring/speech/grade.js';\nimport type {\n ActivityType,\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n ReadAloudData,\n WrittenResponseData,\n} from '../types/activity.js';\nimport type {\n AiCoachingRequest,\n AiCritiqueRequest,\n AiDraftSource,\n AiDraftsRequest,\n AiDraftType,\n AiExplanationRequest,\n AiGradingRequest,\n AiGroupInput,\n AiHintRequest,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport type { ItemGroup } from '../types/item-group.js';\nimport type { SpeechAssessment, SpeechWord } from '../types/speech.js';\n\n/**\n * The items the cases are built from. Small, ordinary language-course content\n * — and deliberately awkward in the places a model slips:\n *\n * - an answer a hint can hardly avoid naming (`Madrid`);\n * - an answer of one short word (`is`), which a hint may write as an ordinary\n * word and must not write beside its neighbour in the passage;\n * - an accented answer (`cañón`), so a hint that spells it without the accent\n * is still caught;\n * - a passage with more than one blank, so a hint can be about the wrong one;\n * - a short essay whose mistakes a model must quote exactly as the learner\n * wrote them — straight apostrophes a model likes to curl, a mistake made\n * twice — so a correction of words nobody wrote is caught;\n * - questions in item groups: a reading passage that prints a question's\n * answer, which a hint must point to without quoting, and a run of blanks\n * whose instructions only the group states;\n * - essays to grade on a rubric of three criteria, two judged on levels and\n * one on a score, so a grade that drops a criterion or leaves a scale is\n * caught.\n */\nconst capital: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-mc',\n title: 'Capital cities',\n question: 'What is the capital of Spain?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'madrid', text: 'Madrid', isCorrect: true },\n { id: 'barcelona', text: 'Barcelona', isCorrect: false },\n { id: 'seville', text: 'Seville', isCorrect: false },\n ],\n};\n\nconst introduction: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-fib',\n title: 'Introducing yourself',\n passage: 'My name {{be}} Rossi and I {{live}} in Madrid.',\n blanks: [\n { id: 'be', acceptedAnswers: ['is'] },\n { id: 'live', acceptedAnswers: ['live'], hint: 'Present simple, first person.' },\n ],\n scoringStrategy: 'partial',\n};\n\nconst landscape: GapSelectData = {\n schemaVersion: '1.0',\n type: 'gap-select',\n id: 'ai-check-gap',\n title: 'Geography',\n locale: 'es-ES',\n passage: 'El río pasa por un {{g1}} muy profundo, y el agua {{g2}} fría.',\n gaps: [\n {\n id: 'g1',\n choices: [\n { id: 'canon', text: 'cañón' },\n { id: 'campo', text: 'campo' },\n ],\n correctChoiceId: 'canon',\n },\n {\n id: 'g2',\n choices: [\n { id: 'esta', text: 'está' },\n { id: 'es', text: 'es' },\n ],\n correctChoiceId: 'esta',\n },\n ],\n scoringStrategy: 'partial',\n};\n\nconst listening: DictationData = {\n schemaVersion: '1.0',\n type: 'dictation',\n id: 'ai-check-dictation',\n title: 'Listen and type',\n transcript: 'The train leaves at half past nine.',\n};\n\n/**\n * A reading group: a passage, a question it answers word for word, and a\n * summary of it. The question's answer is printed in the passage, so a hint\n * that quotes the passage where it answers is refused.\n */\nconst TIDES =\n 'Along most coasts the tide comes in twice a day. It is pulled by the moon: as the Earth ' +\n 'turns, the ocean nearest the moon bulges towards it, and so does the ocean on the far side. ' +\n 'The highest tides, called spring tides, come when the sun and the moon line up.';\n\nconst springTides: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-tides',\n title: 'Spring tides',\n question: 'When do the highest tides come?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'line-up', text: 'When the sun and the moon line up', isCorrect: true },\n { id: 'full-moon', text: 'Only at full moon', isCorrect: false },\n { id: 'winter', text: 'In winter', isCorrect: false },\n ],\n};\n\nconst tidesSummary: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n};\n\nconst tides: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-group',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummary],\n};\n\n/** A part whose questions share instructions the questions themselves never state. */\nconst formation: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-formation',\n title: 'Word formation',\n passage: 'She smiled {{a}} when she heard the news. (HAPPY)',\n blanks: [{ id: 'a', acceptedAnswers: ['happily'] }],\n scoringStrategy: 'all-or-nothing',\n};\n\nconst formationPart: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-formation-part',\n title: 'Part 1 — Word formation',\n stimulus: {\n id: 'ai-check-formation-instructions',\n kind: 'text',\n body: 'Use the word in capitals at the end of each sentence to form a word that fits the gap.',\n },\n items: [formation],\n};\n\nconst weekend: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n { name: 'Grammar', description: 'Past tenses, articles, agreement', weight: 2 },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n { name: 'Task', description: 'Says what the learner did, in order', weight: 1 },\n ],\n },\n};\n\n/**\n * The weekend task set as an exam question: graded on three criteria, two on\n * levels and one on a score. Three, so a prompt that names only the ones it\n * expects drops one; two scales, so a grade must keep to each.\n */\nconst weekendExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing-exam',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n {\n name: 'Task',\n description: 'Says what the learner did last weekend, in order',\n weight: 1,\n levels: [\n { label: 'Not met', description: 'Not about last weekend' },\n { label: 'Partly met', description: 'About the weekend, but thin or out of order' },\n { label: 'Met', description: 'Says what they did, in order' },\n ],\n },\n {\n name: 'Grammar',\n description: 'Past tenses, articles, agreement',\n weight: 2,\n levels: [\n { label: 'Limited', description: 'Errors in most sentences, some blocking meaning' },\n { label: 'Basic', description: 'Frequent errors in past tenses and articles' },\n { label: 'Good', description: 'Some errors, none that blocks meaning' },\n {\n label: 'Very good',\n description: 'Past tenses, articles and agreement almost always right',\n },\n ],\n },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n ],\n },\n};\n\n/** The tides summary set as an exam question, graded against its group's passage. */\nconst tidesSummaryExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary-exam',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n rubric: {\n criteria: [\n {\n name: 'Content',\n description: 'What the text says, and nothing it does not',\n weight: 2,\n levels: [\n { label: 'Wrong', description: 'Misstates what the text says' },\n { label: 'Partly right', description: 'Gets one of its points wrong, or leaves it out' },\n { label: 'Right', description: 'States its points as the text does' },\n ],\n },\n { name: 'Language', description: \"Clear sentences, in the learner's own words\", weight: 1 },\n ],\n },\n};\n\nconst tidesExam: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-exam',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-exam-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummaryExam],\n};\n\nconst weather: ReadAloudData = {\n schemaVersion: '1.0',\n type: 'read-aloud',\n id: 'ai-check-reading',\n title: 'Read the forecast aloud',\n referenceText: 'The weather is warm in the north.',\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: {\n dimensions: [\n { name: 'accuracy', weight: 2 },\n { name: 'fluency', weight: 1 },\n ],\n },\n};\n\n/** A word of the forecast as an engine reports it. */\nconst said = (text: string, over: Partial<SpeechWord> = {}): SpeechWord => ({\n text,\n error: 'none',\n accuracy: 92,\n ...over,\n});\n\n/**\n * An engine's assessment of the forecast. With `slips`, \"weather\" is\n * mispronounced — its \"th\" heard as /d/ or /z/ — and \"north\" is left out: the\n * marks a model must coach, and no others.\n */\nfunction forecast(slips: boolean): SpeechAssessment {\n return {\n assessmentVersion: '1.0',\n status: 'assessed',\n task: 'scripted',\n locale: 'en-US',\n referenceText: weather.referenceText,\n recordingKey: 'ai-check-take',\n assessor: { kind: 'auto', id: 'pronunciation-engine' },\n scale: 100,\n scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },\n miscue: 'assessor',\n phonemeAlphabet: 'ipa',\n words: [\n said('The'),\n slips\n ? said('weather', {\n accuracy: 38,\n error: 'mispronunciation',\n phonemes: [\n { symbol: 'w', accuracy: 90 },\n { symbol: 'ɛ', accuracy: 85 },\n {\n symbol: 'ð',\n accuracy: 12,\n heardAs: [\n { symbol: 'd', score: 64 },\n { symbol: 'z', score: 21 },\n { symbol: 'ð', score: 12 },\n ],\n },\n { symbol: 'ɚ', accuracy: 80 },\n ],\n })\n : said('weather'),\n said('is'),\n said('warm'),\n said('in'),\n said('the'),\n slips ? { text: 'north', error: 'omission' } : said('north'),\n ],\n };\n}\n\n/**\n * One call to make against a host's port, and what a learner was looking at\n * when the SDK would have made it.\n */\nexport interface AiCheckCase {\n /** Stable across releases, so a host can name one in an allow-list or a report. */\n id: string;\n /** Which port the case is for. */\n feature:\n | 'explanation'\n | 'hint'\n | 'writing-feedback'\n | 'pronunciation-coaching'\n | 'item-critique'\n | 'draft-generation'\n | 'grading';\n /** What the learner did, in a few words, for a report a person reads. */\n about: string;\n /** The request, built by the SDK exactly as a component builds it. */\n request:\n | AiExplanationRequest\n | AiHintRequest\n | AiWritingFeedbackRequest\n | AiCoachingRequest\n | AiCritiqueRequest\n | AiDraftsRequest\n | AiGradingRequest;\n}\n\nconst chose = (ids: string[]): LearnerResponse => ({\n type: 'multiple-choice',\n selectedOptionIds: ids,\n});\nconst typed = (answers: Record<string, string>): LearnerResponse => ({\n type: 'fill-in-the-blanks',\n answers,\n});\nconst picked = (choices: Record<string, string>): LearnerResponse => ({\n type: 'gap-select',\n selections: choices,\n});\nconst wrote = (text: string): LearnerResponse => ({ type: 'dictation', text });\nconst drafted = (text: string): LearnerResponse => ({\n type: 'written-response',\n text,\n wordCount: 0,\n});\n\n/**\n * Items an author asks a model to review. Each is valid, and each but the last\n * has a flaw a rule cannot see: a distractor that is botanically a right\n * answer, and a task far above the level it is set for.\n */\nconst fruit: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-fruit',\n title: 'Food words',\n question: 'Which of these is a fruit?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'f1', text: 'Apple', isCorrect: true },\n { id: 'f2', text: 'Carrot', isCorrect: false },\n { id: 'f3', text: 'Tomato', isCorrect: false },\n ],\n};\n\nconst tooHard: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-too-hard',\n title: 'Cities',\n prompt: 'Discuss the socioeconomic ramifications of rapid urbanisation in developing economies.',\n minWords: 40,\n maxWords: 80,\n};\n\nconst cleanCloze: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-clean-cloze',\n title: 'Daily routine',\n passage: 'Every morning I {{1}} up at seven and {{2}} a shower.',\n blanks: [\n { id: '1', acceptedAnswers: ['get', 'wake'] },\n { id: '2', acceptedAnswers: ['take', 'have'] },\n ],\n scoringStrategy: 'partial',\n};\n\nfunction critiqueCase(\n id: string,\n about: string,\n type: ActivityType,\n draft: unknown,\n extra: { level?: string; authorLocale?: string } = {},\n): AiCheckCase {\n const request = aiCritiqueRequest({ type, draft, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no critique request for \"${id}\"`);\n }\n return { id, feature: 'item-critique', about, request };\n}\n\n/** A short reading to draft from: plain, and with facts a question can be asked about. */\nconst MARKET =\n 'Maria lives in Seville, in the south of Spain. Every Saturday she walks to the market with her ' +\n 'grandmother. They buy oranges, fresh bread and a little cheese. On the way home they stop at a ' +\n 'café, where Maria drinks hot chocolate and her grandmother reads the newspaper.';\n\n/** The same reading as a video's captions, with times in seconds. */\nconst MARKET_CAPTIONS = [\n { start: 0, end: 4.2, text: 'Maria lives in Seville, in the south of Spain.' },\n { start: 4.2, end: 8.9, text: 'Every Saturday she walks to the market with her grandmother.' },\n { start: 8.9, end: 12.6, text: 'They buy oranges, fresh bread and a little cheese.' },\n { start: 12.6, end: 18.1, text: 'On the way home they stop at a café.' },\n];\n\nfunction draftsCase(\n id: string,\n about: string,\n types: AiDraftType[],\n source: AiDraftSource,\n extra: Omit<Parameters<typeof aiDraftsRequest>[0], 'types' | 'source'> = {},\n): AiCheckCase {\n const request = aiDraftsRequest({ types, source, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no drafts request for \"${id}\"`);\n }\n return { id, feature: 'draft-generation', about, request };\n}\n\n/**\n * What a host sets on every read-aloud it drafts: the SDK has no default for a\n * recording's length or the dimensions a reading is graded on.\n */\nconst READ_ALOUD_SETTINGS = {\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: { dimensions: [{ name: 'accuracy', weight: 1 }] },\n};\n\n/** Builds a case, failing loudly rather than shipping a case with no request. */\nfunction explanationCase(\n id: string,\n about: string,\n data: Parameters<typeof aiExplanationRequest>[0]['data'],\n response: LearnerResponse,\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiExplanationRequest({ data, response, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no explanation request for \"${id}\"`);\n }\n return { id, feature: 'explanation', about, request };\n}\n\nfunction hintCase(\n id: string,\n about: string,\n data: Parameters<typeof aiHintRequest>[0]['data'],\n response: LearnerResponse,\n previousHints: string[] = [],\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiHintRequest({ data, response, previousHints, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no hint request for \"${id}\"`);\n }\n return { id, feature: 'hint', about, request };\n}\n\nfunction writingCase(\n id: string,\n about: string,\n text: string,\n previousFeedback: string[] = [],\n learnerLocale?: string,\n task: { data: WrittenResponseData; group?: AiGroupInput } = { data: weekend },\n): AiCheckCase {\n const request = aiWritingFeedbackRequest({\n data: task.data,\n response: drafted(text),\n previousFeedback,\n ...(task.group ? { group: task.group } : {}),\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no writing feedback request for \"${id}\"`);\n }\n return { id, feature: 'writing-feedback', about, request };\n}\n\nfunction gradingCase(\n id: string,\n about: string,\n text: string,\n extra: { data?: WrittenResponseData; group?: AiGroupInput; learnerLocale?: string } = {},\n): AiCheckCase {\n const built = aiGradingRequest({\n data: extra.data ?? weekendExam,\n response: drafted(text),\n ...(extra.group ? { group: extra.group } : {}),\n ...(extra.learnerLocale !== undefined ? { learnerLocale: extra.learnerLocale } : {}),\n });\n if (!built.ok) {\n throw new Error(`ai-check: no grading request for \"${id}\": ${built.problem}`);\n }\n return { id, feature: 'grading', about, request: built.request };\n}\n\nfunction coachingCase(\n id: string,\n about: string,\n marks: { assessment?: SpeechAssessment; fromGradeOnly?: boolean },\n learnerLocale?: string,\n): AiCheckCase {\n const assessment = marks.assessment ?? forecast(true);\n const grade = gradeReadAloud(\n weather,\n { type: 'read-aloud', recording: { key: 'ai-check-take', mimeType: 'audio/wav' } },\n assessment,\n {\n measured: { durationMs: 4000, voicedMs: 3000 },\n plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 },\n },\n );\n if ('unscorable' in grade) {\n throw new Error(`ai-check: the reading for \"${id}\" did not grade: ${grade.reason}`);\n }\n const request = aiCoachingRequest({\n data: weather,\n ...(marks.fromGradeOnly === true ? {} : { assessment }),\n grade,\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no coaching request for \"${id}\"`);\n }\n return { id, feature: 'pronunciation-coaching', about, request };\n}\n\n/**\n * The cases {@link runAiCheck} runs: every type the SDK explains, answered\n * right, wrong and — where a type can be — partly right; every type it hints\n * for, before an answer and after a wrong one; drafts of an essay, with\n * mistakes, without, and revised; and essays to grade, on the task, with\n * mistakes to quote, off the task, and against a passage.\n *\n * They are ordinary calls. What makes them a test is that the SDK knows the\n * answer to each, so it can check what your model writes about it.\n *\n * A fresh array each call, so a caller may filter, slice or add to it without\n * changing what the next caller gets.\n */\nexport function aiCheckCases(): AiCheckCase[] {\n return [\n explanationCase('mc-correct', 'Chose the right city', capital, chose(['madrid'])),\n explanationCase('mc-wrong', 'Chose Barcelona', capital, chose(['barcelona'])),\n explanationCase('mc-blank', 'Chose nothing and submitted', capital, chose([])),\n explanationCase(\n 'fib-correct',\n 'Filled both blanks correctly',\n introduction,\n typed({ be: 'is', live: 'live' }),\n ),\n explanationCase(\n 'fib-partly',\n 'One blank right, one wrong',\n introduction,\n typed({ be: 'is', live: 'lives' }),\n ),\n explanationCase(\n 'fib-wrong',\n 'Both blanks wrong',\n introduction,\n typed({ be: 'are', live: 'living' }),\n ),\n explanationCase(\n 'gap-partly',\n 'One gap right, one wrong, in Spanish',\n landscape,\n picked({ g1: 'canon', g2: 'es' }),\n ),\n explanationCase(\n 'gap-wrong',\n 'Both gaps wrong, in Spanish',\n landscape,\n picked({ g1: 'campo', g2: 'es' }),\n ),\n explanationCase(\n 'dictation-near',\n 'Typed the sentence with two words wrong',\n listening,\n wrote('The train leaves at half past five'),\n ),\n explanationCase(\n 'dictation-correct',\n 'Typed the sentence correctly',\n listening,\n wrote('The train leaves at half past nine.'),\n ),\n explanationCase(\n 'mc-reading-wrong',\n \"Chose 'Only at full moon', on a question about its group's passage\",\n springTides,\n chose(['full-moon']),\n tides,\n ),\n hintCase('mc-hint-1', 'Has not answered yet', capital, chose([])),\n hintCase('mc-hint-2', 'Asked again after choosing Seville', capital, chose(['seville']), [\n 'Think about where the government sits.',\n ]),\n hintCase('fib-hint-1', 'Both blanks empty', introduction, typed({})),\n hintCase(\n 'fib-hint-2',\n 'Asked again with one blank wrong',\n introduction,\n typed({ be: 'are', live: '' }),\n ['Look again at the first blank.'],\n ),\n hintCase('gap-hint-1', 'Neither gap chosen, in Spanish', landscape, picked({})),\n hintCase(\n 'gap-hint-2',\n 'Asked again with the first gap wrong',\n landscape,\n picked({ g1: 'campo' }),\n ['Piensa en la forma del terreno.'],\n ),\n hintCase(\n 'mc-reading-hint',\n \"Has not answered a question whose answer its group's passage prints\",\n springTides,\n chose([]),\n [],\n tides,\n ),\n hintCase(\n 'fib-formation-hint',\n 'A word-formation blank, under the instructions its group gives',\n formation,\n typed({}),\n [],\n formationPart,\n ),\n writingCase(\n 'writing-mistakes',\n 'A first draft with tense, article and agreement mistakes',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n writingCase(\n 'writing-clean',\n 'A first draft with no mistakes to correct',\n 'Last weekend I went to the market and bought an apple. My friends were there too, so we stayed until the evening. Then we went to the cinema.',\n ),\n writingCase(\n 'writing-revised',\n 'A second draft that fixed some mistakes',\n \"Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.\",\n ['Check your past tenses: \"go\" and \"buyed\" should be past forms.'],\n ),\n writingCase(\n 'writing-feedback-in-spanish',\n 'A first draft, with feedback asked for in Spanish',\n 'Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.',\n [],\n 'es',\n ),\n writingCase(\n 'writing-summary',\n \"A summary of its group's passage that gets the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n [],\n undefined,\n { data: tidesSummary, group: tides },\n ),\n coachingCase('coaching-slips', 'A reading with a mispronounced \"th\" and a word left out', {\n assessment: forecast(true),\n }),\n coachingCase('coaching-clean', 'A reading the engine marked correct throughout', {\n assessment: forecast(false),\n }),\n coachingCase(\n 'coaching-from-grade',\n 'The same slips, from a stored grade: marks without sounds',\n { fromGradeOnly: true },\n ),\n coachingCase(\n 'coaching-in-spanish',\n 'The same slips, with coaching asked for in Spanish',\n { assessment: forecast(true) },\n 'es',\n ),\n critiqueCase(\n 'critique-second-answer',\n 'A question whose distractor \"Tomato\" is botanically a fruit',\n 'multiple-choice',\n fruit,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-level',\n 'A writing task far above the A1 level it is set for',\n 'written-response',\n tooHard,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-clean',\n 'A cloze with nothing wrong in it',\n 'fill-in-the-blanks',\n cleanCloze,\n {\n level: 'A2',\n },\n ),\n critiqueCase(\n 'critique-in-spanish',\n 'The fruit question, reviewed in Spanish for its author',\n 'multiple-choice',\n fruit,\n { level: 'A1', authorLocale: 'es' },\n ),\n draftsCase(\n 'drafts-questions',\n 'Four multiple-choice questions on a short reading',\n ['multiple-choice'],\n { kind: 'passage', text: MARKET },\n { count: 4, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-cloze',\n 'A cloze on the same reading, from its transcript',\n ['fill-in-the-blanks'],\n { kind: 'transcript', text: MARKET },\n { count: 1, level: 'A2', locale: 'en', instructions: 'Blank the verbs.' },\n ),\n draftsCase(\n 'drafts-gaps',\n 'A choose-the-word item on the same reading',\n ['gap-select'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-writing',\n 'A writing task that follows on from the reading',\n ['written-response'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-video',\n 'A video quiz from timed captions: multiple choice, then dictation, then read-aloud, as many as the video is worth',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'captions', cues: MARKET_CAPTIONS },\n { level: 'A2', locale: 'en', settings: { 'read-aloud': READ_ALOUD_SETTINGS } },\n ),\n draftsCase(\n 'drafts-script',\n 'The same video from its script alone, with no times, and what the author asked for',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'transcript', text: MARKET },\n {\n level: 'A2',\n locale: 'en',\n instructions: 'Focus on what Maria and her grandmother buy.',\n settings: { 'read-aloud': READ_ALOUD_SETTINGS },\n },\n ),\n gradingCase(\n 'grading-on-task',\n 'An essay that does the task, with one slip in its tenses',\n 'Last weekend I went to the market with my sister and we bought some apples. In the afternoon we watched a film at home, and on Sunday I visit my grandmother.',\n ),\n gradingCase(\n 'grading-mistakes',\n 'An essay with tense, article and agreement mistakes, each to quote as written',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n gradingCase(\n 'grading-off-task',\n 'An essay about something other than last weekend',\n 'My favourite food is pizza. I like it with cheese and tomato, and I eat it every Friday with my family at a small restaurant near our house.',\n ),\n gradingCase(\n 'grading-summary',\n \"A summary graded against its group's passage, getting the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n { data: tidesSummaryExam, group: tidesExam },\n ),\n gradingCase(\n 'grading-in-spanish',\n 'The essay with mistakes, with comments asked for in Spanish',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n { learnerLocale: 'es' },\n ),\n ];\n}\n","/**\n * A test kit for the prompts behind your AI ports.\n *\n * The SDK cannot test your model, and will not try. What it can do is make the\n * calls a learner's questions would make — on items whose answers it knows —\n * and run the same checks it runs before a learner sees anything. What comes\n * back is a count of what would have been shown and what would have been\n * refused, and why.\n *\n * Run it where you change a prompt or a model, in your own CI, with your own\n * key. It is a separate entry point (`@intellectif/lk-core/ai-check`) because\n * it is for your test run, not for a learner's browser: nothing here is\n * imported by anything a page renders.\n *\n * ```ts\n * import { aiCheckCases, formatAiCheckReport, runAiCheck } from '@intellectif/lk-core/ai-check';\n *\n * const report = await runAiCheck({\n * explain: (request) => callMyModel(EXPLAIN_PROMPT, request),\n * hint: (request) => callMyModel(HINT_PROMPT, request),\n * writingFeedback: (request) => callMyModel(FEEDBACK_PROMPT, request),\n * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),\n * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),\n * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),\n * grading: (request) => callMyModel(GRADING_PROMPT, request),\n * });\n * console.log(formatAiCheckReport(report));\n * if (report.refused > 0 || report.errors > 0) {\n * process.exitCode = 1;\n * }\n * ```\n *\n * A refusal is a real failure, not a warning: it is an answer a learner would\n * have asked for and not received. `reveals-answer` on a hint case is the one\n * to take most seriously — that prompt gives answers away. Next come\n * `misquotes-answer` on a writing case — that prompt corrects words the learner\n * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches\n * words the engine heard as right, or sounds it never reported. On a grading\n * case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one\n * that leaves a criterion out grades a different task, and one off a\n * criterion's scale grades on a scale the author did not write.\n *\n * A grading case the SDK accepts is a reply of the right shape, not a right\n * grade: whether a model grades as your teachers do is what a calibration\n * measures (`calibrationReport`), on your own responses.\n */\nimport { checkAiExplanation, checkAiHint } from '../ai.js';\nimport { checkAiCoaching } from '../ai-coaching.js';\nimport { checkAiCritique } from '../ai-critique.js';\nimport { checkAiDrafts } from '../ai-drafts.js';\nimport { checkAiGrading } from '../ai-grading.js';\nimport { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\n AiCritique,\n AiCritiqueRequest,\n AiDrafts,\n AiDraftsRequest,\n AiExplanationRequest,\n AiGradingJudgement,\n AiGradingRequest,\n AiHintRequest,\n AiRefusal,\n AiTextResult,\n AiWritingFeedback,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport { type AiCheckCase, aiCheckCases } from './cases.js';\n\nexport { type AiCheckCase, aiCheckCases } from './cases.js';\n\n/**\n * Your ports, as this kit calls them. The same functions\n * `<LkAiProvider ai={…}>` takes, minus the `AbortSignal` a browser passes —\n * nothing here abandons a call.\n *\n * Leave one out and its cases are skipped rather than failed.\n */\nexport interface AiCheckPorts {\n explain?(request: AiExplanationRequest): Promise<unknown> | unknown;\n hint?(request: AiHintRequest): Promise<unknown> | unknown;\n writingFeedback?(request: AiWritingFeedbackRequest): Promise<unknown> | unknown;\n pronunciationCoaching?(request: AiCoachingRequest): Promise<unknown> | unknown;\n /** Reviews an item for its author: the port an editor's \"Review with AI\" calls. */\n critique?(request: AiCritiqueRequest): Promise<unknown> | unknown;\n /** Drafts items from a source, for an author to approve. */\n drafts?(request: AiDraftsRequest): Promise<unknown> | unknown;\n /** Grades a submitted essay on its rubric: a server's port, which no component calls. */\n grading?(request: AiGradingRequest): Promise<unknown> | unknown;\n}\n\n/** What happened on one case. */\nexport interface AiCheckResult {\n case: AiCheckCase;\n /** Whether a learner would have been shown what came back. */\n ok: boolean;\n /** Why it would not have been shown. */\n refusal?: AiRefusal;\n /** The port threw or rejected. Its message, as a string. */\n error?: string;\n /**\n * What came back, once the SDK had read it — so a refusal can be read beside\n * the text that caused it. Absent when the port failed or answered with\n * nothing the SDK could read.\n */\n result?: AiTextResult;\n /** For a writing case a learner would have been shown: the feedback, as the SDK accepted it. */\n feedback?: AiWritingFeedback;\n /** For a coaching case a learner would have been shown: the coaching, as the SDK accepted it. */\n coaching?: AiCoaching;\n /** For a critique case an author would have been shown: the findings, as the SDK accepted them. */\n critique?: AiCritique;\n /**\n * For a drafts case the SDK read: the drafts, each with what `validateDraft`\n * and the item critic say of it. A reply read is not a good one — check how\n * many came back `complete` and without warnings.\n */\n drafts?: AiDrafts;\n /**\n * For a grading case the SDK accepted: the judgement, as checked. A reply of\n * the right shape, not a right grade — `gradeFromJudgement` turns it into one.\n */\n grading?: AiGradingJudgement;\n /** How long the call took, in milliseconds. */\n ms: number;\n}\n\n/** What a run came to. */\nexport interface AiCheckReport {\n /** Cases run: the ones a port was supplied for. */\n total: number;\n /** Cases whose answer a learner would have been shown. */\n shown: number;\n /** Cases whose answer the SDK refused. */\n refused: number;\n /** Cases where the port itself failed. */\n errors: number;\n /** Cases skipped because no port was supplied for that feature. */\n skipped: number;\n /** How many refusals of each reason. */\n byRefusal: Record<AiRefusal, number>;\n results: AiCheckResult[];\n}\n\nexport interface AiCheckOptions {\n /**\n * The cases to run. Defaults to {@link aiCheckCases}. Pass a filtered list to\n * run one type, or your own cases — built with `aiExplanationRequest` and\n * `aiHintRequest` on your own items, which is how you test a prompt against\n * the content your learners actually see.\n */\n cases?: readonly AiCheckCase[];\n /**\n * How many cases to have in flight at once. Default 1: a run in CI hits a\n * rate limit far more often than it runs out of patience.\n */\n concurrency?: number;\n}\n\n/**\n * Every refusal, in the order a report lists them. A map rather than a list, so\n * a refusal added to `AiRefusal` and not here fails to compile instead of going\n * uncounted.\n */\nconst REFUSAL_ORDER = {\n malformed: true,\n empty: true,\n 'too-long': true,\n 'contradicts-grade': true,\n 'reveals-answer': true,\n 'misquotes-answer': true,\n 'contradicts-marks': true,\n 'contradicts-item': true,\n 'misses-criteria': true,\n 'out-of-scale': true,\n} as const satisfies Record<AiRefusal, true>;\n\nconst REFUSALS = Object.keys(REFUSAL_ORDER) as AiRefusal[];\n\n/** The port a case is for. */\nfunction portFor(\n ports: AiCheckPorts,\n one: AiCheckCase,\n): ((request: never) => Promise<unknown> | unknown) | undefined {\n switch (one.request.feature) {\n case 'explanation':\n return ports.explain as never;\n case 'hint':\n return ports.hint as never;\n case 'pronunciation-coaching':\n return ports.pronunciationCoaching as never;\n case 'item-critique':\n return ports.critique as never;\n case 'draft-generation':\n return ports.drafts as never;\n case 'grading':\n return ports.grading as never;\n default:\n return ports.writingFeedback as never;\n }\n}\n\n/** Milliseconds, from whichever clock this runtime has. */\nconst now = (): number => Date.now();\n\n/** Runs one case: calls the port, times it, and checks what came back. */\nasync function runCase(ports: AiCheckPorts, one: AiCheckCase): Promise<AiCheckResult> {\n const started = now();\n const port = portFor(ports, one);\n if (port === undefined) {\n throw new Error(`ai-check: no port for \"${one.id}\"`);\n }\n let raw: unknown;\n try {\n raw = await port(one.request as never);\n } catch (error) {\n return {\n case: one,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n ms: now() - started,\n };\n }\n const ms = now() - started;\n if (one.request.feature === 'grading') {\n const checked = checkAiGrading(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, grading: checked.judgement, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'draft-generation') {\n let next = 0;\n const checked = checkAiDrafts(raw, one.request, {\n newId: () => {\n next += 1;\n return `ai-check-${next}`;\n },\n });\n return checked.ok\n ? { case: one, ok: true, drafts: checked.drafts, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'item-critique') {\n const checked = checkAiCritique(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, critique: checked.critique, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'pronunciation-coaching') {\n const checked = checkAiCoaching(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.coaching;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n coaching: checked.coaching,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'writing-feedback') {\n const checked = checkAiWritingFeedback(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.feedback;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n feedback: checked.feedback,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n const checked =\n one.request.feature === 'explanation'\n ? checkAiExplanation(raw, one.request)\n : checkAiHint(raw, one.request);\n if (checked.ok) {\n return { case: one, ok: true, result: checked.result, ms };\n }\n return refused(one, raw, checked.refusal, ms);\n}\n\n/** A case whose answer was refused, with the text that came back when there was one to read. */\nfunction refused(one: AiCheckCase, raw: unknown, refusal: AiRefusal, ms: number): AiCheckResult {\n const text =\n typeof (raw as { text?: unknown })?.text === 'string' ? (raw as AiTextResult) : undefined;\n return {\n case: one,\n ok: false,\n refusal,\n ...(text !== undefined ? { result: text } : {}),\n ms,\n };\n}\n\n/**\n * Calls your ports on every case and reports what a learner would have seen.\n *\n * It never throws for a failing case: a port that rejects is an `error` in the\n * report, so one bad call cannot hide the rest.\n */\nexport async function runAiCheck(\n ports: AiCheckPorts,\n options: AiCheckOptions = {},\n): Promise<AiCheckReport> {\n const all = options.cases ?? aiCheckCases();\n const asked = Math.trunc(options.concurrency ?? 1);\n const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;\n const runnable = all.filter((one) => portFor(ports, one) !== undefined);\n const results: AiCheckResult[] = new Array(runnable.length);\n let next = 0;\n const lane = async (): Promise<void> => {\n while (next < runnable.length) {\n const mine = next;\n next += 1;\n const one = runnable[mine];\n if (one === undefined) {\n return;\n }\n results[mine] = await runCase(ports, one);\n }\n };\n await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));\n\n const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0])) as Record<\n AiRefusal,\n number\n >;\n let shown = 0;\n let refused = 0;\n let errors = 0;\n for (const result of results) {\n if (result.ok) {\n shown += 1;\n } else if (result.refusal !== undefined) {\n refused += 1;\n byRefusal[result.refusal] += 1;\n } else {\n errors += 1;\n }\n }\n return {\n total: results.length,\n shown,\n refused,\n errors,\n skipped: all.length - runnable.length,\n byRefusal,\n results,\n };\n}\n\n/**\n * The report as a few lines for a CI log: the counts, then every case that\n * failed, with what came back.\n *\n * The refused text is printed — this is your own test run, not a learner's\n * screen, and a hint that gives the answer away is only fixable if you can\n * read it.\n */\nexport function formatAiCheckReport(report: AiCheckReport): string {\n const lines: string[] = [];\n const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);\n lines.push(\n `ai-check: ${report.shown}/${report.total} shown, ${report.refused} refused, ${report.errors} failed` +\n `${report.skipped > 0 ? `, ${report.skipped} skipped (no port)` : ''} — slowest ${slowest} ms`,\n );\n for (const reason of REFUSALS) {\n const count = report.byRefusal[reason];\n if (count > 0) {\n lines.push(` ${reason}: ${count}`);\n }\n }\n for (const result of report.results) {\n if (result.ok) {\n continue;\n }\n const why = result.refusal ?? `port failed (${result.error ?? 'no message'})`;\n lines.push(` ✗ ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);\n if (result.result !== undefined) {\n lines.push(` ${result.result.text.replace(/\\s+/g, ' ').slice(0, 200)}`);\n }\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-check.js","names":[],"sources":["../src/ai-check/cases.ts","../src/ai-check/index.ts"],"sourcesContent":["import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport { aiCoachingRequest } from '../ai-coaching.js';\nimport { aiCritiqueRequest } from '../ai-critique.js';\nimport { aiDraftsRequest } from '../ai-drafts.js';\nimport { aiGradingRequest } from '../ai-grading.js';\nimport { aiWritingFeedbackRequest } from '../ai-writing.js';\nimport { gradeReadAloud } from '../scoring/speech/grade.js';\nimport type {\n ActivityType,\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n ReadAloudData,\n WrittenResponseData,\n} from '../types/activity.js';\nimport type {\n AiCoachingRequest,\n AiCritiqueRequest,\n AiDraftSource,\n AiDraftsRequest,\n AiDraftType,\n AiExplanationRequest,\n AiGradingRequest,\n AiGroupInput,\n AiHintRequest,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport type { ItemGroup } from '../types/item-group.js';\nimport type { SpeechAssessment, SpeechWord } from '../types/speech.js';\n\n/**\n * The items the cases are built from. Small, ordinary language-course content\n * — and deliberately awkward in the places a model slips:\n *\n * - an answer a hint can hardly avoid naming (`Madrid`);\n * - an answer of one short word (`is`), which a hint may write as an ordinary\n * word and must not write beside its neighbour in the passage;\n * - an accented answer (`cañón`), so a hint that spells it without the accent\n * is still caught;\n * - a passage with more than one blank, so a hint can be about the wrong one;\n * - a short essay whose mistakes a model must quote exactly as the learner\n * wrote them — straight apostrophes a model likes to curl, a mistake made\n * twice — so a correction of words nobody wrote is caught;\n * - questions in item groups: a reading passage that prints a question's\n * answer, which a hint must point to without quoting, and a run of blanks\n * whose instructions only the group states;\n * - the same blank served alone, as an adaptive test serves it, whose task\n * only its own instructions state;\n * - essays to grade on a rubric of three criteria, two judged on levels and\n * one on a score, so a grade that drops a criterion or leaves a scale is\n * caught.\n */\nconst capital: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-mc',\n title: 'Capital cities',\n question: 'What is the capital of Spain?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'madrid', text: 'Madrid', isCorrect: true },\n { id: 'barcelona', text: 'Barcelona', isCorrect: false },\n { id: 'seville', text: 'Seville', isCorrect: false },\n ],\n};\n\nconst introduction: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-fib',\n title: 'Introducing yourself',\n passage: 'My name {{be}} Rossi and I {{live}} in Madrid.',\n blanks: [\n { id: 'be', acceptedAnswers: ['is'] },\n { id: 'live', acceptedAnswers: ['live'], hint: 'Present simple, first person.' },\n ],\n scoringStrategy: 'partial',\n};\n\nconst landscape: GapSelectData = {\n schemaVersion: '1.0',\n type: 'gap-select',\n id: 'ai-check-gap',\n title: 'Geography',\n locale: 'es-ES',\n passage: 'El río pasa por un {{g1}} muy profundo, y el agua {{g2}} fría.',\n gaps: [\n {\n id: 'g1',\n choices: [\n { id: 'canon', text: 'cañón' },\n { id: 'campo', text: 'campo' },\n ],\n correctChoiceId: 'canon',\n },\n {\n id: 'g2',\n choices: [\n { id: 'esta', text: 'está' },\n { id: 'es', text: 'es' },\n ],\n correctChoiceId: 'esta',\n },\n ],\n scoringStrategy: 'partial',\n};\n\nconst listening: DictationData = {\n schemaVersion: '1.0',\n type: 'dictation',\n id: 'ai-check-dictation',\n title: 'Listen and type',\n transcript: 'The train leaves at half past nine.',\n};\n\n/**\n * A reading group: a passage, a question it answers word for word, and a\n * summary of it. The question's answer is printed in the passage, so a hint\n * that quotes the passage where it answers is refused.\n */\nconst TIDES =\n 'Along most coasts the tide comes in twice a day. It is pulled by the moon: as the Earth ' +\n 'turns, the ocean nearest the moon bulges towards it, and so does the ocean on the far side. ' +\n 'The highest tides, called spring tides, come when the sun and the moon line up.';\n\nconst springTides: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-tides',\n title: 'Spring tides',\n question: 'When do the highest tides come?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'line-up', text: 'When the sun and the moon line up', isCorrect: true },\n { id: 'full-moon', text: 'Only at full moon', isCorrect: false },\n { id: 'winter', text: 'In winter', isCorrect: false },\n ],\n};\n\nconst tidesSummary: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n};\n\nconst tides: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-group',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummary],\n};\n\n/** A part whose questions share instructions the questions themselves never state. */\nconst formation: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-formation',\n title: 'Word formation',\n passage: 'She smiled {{a}} when she heard the news. (HAPPY)',\n blanks: [{ id: 'a', acceptedAnswers: ['happily'] }],\n scoringStrategy: 'all-or-nothing',\n};\n\nconst formationPart: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-formation-part',\n title: 'Part 1 — Word formation',\n stimulus: {\n id: 'ai-check-formation-instructions',\n kind: 'text',\n body: 'Use the word in capitals at the end of each sentence to form a word that fits the gap.',\n },\n items: [formation],\n};\n\n/** The same blank served alone, as an adaptive test serves it: the task is in its own instructions. */\nconst formationAlone: FillInTheBlanksData = {\n ...formation,\n id: 'ai-check-formation-alone',\n instructions:\n 'Use the word in capitals at the end of the sentence to form a word that fits the gap.',\n};\n\nconst weekend: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n { name: 'Grammar', description: 'Past tenses, articles, agreement', weight: 2 },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n { name: 'Task', description: 'Says what the learner did, in order', weight: 1 },\n ],\n },\n};\n\n/**\n * The weekend task set as an exam question: graded on three criteria, two on\n * levels and one on a score. Three, so a prompt that names only the ones it\n * expects drops one; two scales, so a grade must keep to each.\n */\nconst weekendExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing-exam',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n {\n name: 'Task',\n description: 'Says what the learner did last weekend, in order',\n weight: 1,\n levels: [\n { label: 'Not met', description: 'Not about last weekend' },\n { label: 'Partly met', description: 'About the weekend, but thin or out of order' },\n { label: 'Met', description: 'Says what they did, in order' },\n ],\n },\n {\n name: 'Grammar',\n description: 'Past tenses, articles, agreement',\n weight: 2,\n levels: [\n { label: 'Limited', description: 'Errors in most sentences, some blocking meaning' },\n { label: 'Basic', description: 'Frequent errors in past tenses and articles' },\n { label: 'Good', description: 'Some errors, none that blocks meaning' },\n {\n label: 'Very good',\n description: 'Past tenses, articles and agreement almost always right',\n },\n ],\n },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n ],\n },\n};\n\n/** The tides summary set as an exam question, graded against its group's passage. */\nconst tidesSummaryExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary-exam',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n rubric: {\n criteria: [\n {\n name: 'Content',\n description: 'What the text says, and nothing it does not',\n weight: 2,\n levels: [\n { label: 'Wrong', description: 'Misstates what the text says' },\n { label: 'Partly right', description: 'Gets one of its points wrong, or leaves it out' },\n { label: 'Right', description: 'States its points as the text does' },\n ],\n },\n { name: 'Language', description: \"Clear sentences, in the learner's own words\", weight: 1 },\n ],\n },\n};\n\nconst tidesExam: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-exam',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-exam-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummaryExam],\n};\n\nconst weather: ReadAloudData = {\n schemaVersion: '1.0',\n type: 'read-aloud',\n id: 'ai-check-reading',\n title: 'Read the forecast aloud',\n referenceText: 'The weather is warm in the north.',\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: {\n dimensions: [\n { name: 'accuracy', weight: 2 },\n { name: 'fluency', weight: 1 },\n ],\n },\n};\n\n/** A word of the forecast as an engine reports it. */\nconst said = (text: string, over: Partial<SpeechWord> = {}): SpeechWord => ({\n text,\n error: 'none',\n accuracy: 92,\n ...over,\n});\n\n/**\n * An engine's assessment of the forecast. With `slips`, \"weather\" is\n * mispronounced — its \"th\" heard as /d/ or /z/ — and \"north\" is left out: the\n * marks a model must coach, and no others.\n */\nfunction forecast(slips: boolean): SpeechAssessment {\n return {\n assessmentVersion: '1.0',\n status: 'assessed',\n task: 'scripted',\n locale: 'en-US',\n referenceText: weather.referenceText,\n recordingKey: 'ai-check-take',\n assessor: { kind: 'auto', id: 'pronunciation-engine' },\n scale: 100,\n scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },\n miscue: 'assessor',\n phonemeAlphabet: 'ipa',\n words: [\n said('The'),\n slips\n ? said('weather', {\n accuracy: 38,\n error: 'mispronunciation',\n phonemes: [\n { symbol: 'w', accuracy: 90 },\n { symbol: 'ɛ', accuracy: 85 },\n {\n symbol: 'ð',\n accuracy: 12,\n heardAs: [\n { symbol: 'd', score: 64 },\n { symbol: 'z', score: 21 },\n { symbol: 'ð', score: 12 },\n ],\n },\n { symbol: 'ɚ', accuracy: 80 },\n ],\n })\n : said('weather'),\n said('is'),\n said('warm'),\n said('in'),\n said('the'),\n slips ? { text: 'north', error: 'omission' } : said('north'),\n ],\n };\n}\n\n/**\n * One call to make against a host's port, and what a learner was looking at\n * when the SDK would have made it.\n */\nexport interface AiCheckCase {\n /** Stable across releases, so a host can name one in an allow-list or a report. */\n id: string;\n /** Which port the case is for. */\n feature:\n | 'explanation'\n | 'hint'\n | 'writing-feedback'\n | 'pronunciation-coaching'\n | 'item-critique'\n | 'draft-generation'\n | 'grading';\n /** What the learner did, in a few words, for a report a person reads. */\n about: string;\n /** The request, built by the SDK exactly as a component builds it. */\n request:\n | AiExplanationRequest\n | AiHintRequest\n | AiWritingFeedbackRequest\n | AiCoachingRequest\n | AiCritiqueRequest\n | AiDraftsRequest\n | AiGradingRequest;\n}\n\nconst chose = (ids: string[]): LearnerResponse => ({\n type: 'multiple-choice',\n selectedOptionIds: ids,\n});\nconst typed = (answers: Record<string, string>): LearnerResponse => ({\n type: 'fill-in-the-blanks',\n answers,\n});\nconst picked = (choices: Record<string, string>): LearnerResponse => ({\n type: 'gap-select',\n selections: choices,\n});\nconst wrote = (text: string): LearnerResponse => ({ type: 'dictation', text });\nconst drafted = (text: string): LearnerResponse => ({\n type: 'written-response',\n text,\n wordCount: 0,\n});\n\n/**\n * Items an author asks a model to review. Each is valid, and each but the last\n * has a flaw a rule cannot see: a distractor that is botanically a right\n * answer, and a task far above the level it is set for.\n */\nconst fruit: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-fruit',\n title: 'Food words',\n question: 'Which of these is a fruit?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'f1', text: 'Apple', isCorrect: true },\n { id: 'f2', text: 'Carrot', isCorrect: false },\n { id: 'f3', text: 'Tomato', isCorrect: false },\n ],\n};\n\nconst tooHard: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-too-hard',\n title: 'Cities',\n prompt: 'Discuss the socioeconomic ramifications of rapid urbanisation in developing economies.',\n minWords: 40,\n maxWords: 80,\n};\n\nconst cleanCloze: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-clean-cloze',\n title: 'Daily routine',\n passage: 'Every morning I {{1}} up at seven and {{2}} a shower.',\n blanks: [\n { id: '1', acceptedAnswers: ['get', 'wake'] },\n { id: '2', acceptedAnswers: ['take', 'have'] },\n ],\n scoringStrategy: 'partial',\n};\n\nfunction critiqueCase(\n id: string,\n about: string,\n type: ActivityType,\n draft: unknown,\n extra: { level?: string; authorLocale?: string } = {},\n): AiCheckCase {\n const request = aiCritiqueRequest({ type, draft, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no critique request for \"${id}\"`);\n }\n return { id, feature: 'item-critique', about, request };\n}\n\n/** A short reading to draft from: plain, and with facts a question can be asked about. */\nconst MARKET =\n 'Maria lives in Seville, in the south of Spain. Every Saturday she walks to the market with her ' +\n 'grandmother. They buy oranges, fresh bread and a little cheese. On the way home they stop at a ' +\n 'café, where Maria drinks hot chocolate and her grandmother reads the newspaper.';\n\n/** The same reading as a video's captions, with times in seconds. */\nconst MARKET_CAPTIONS = [\n { start: 0, end: 4.2, text: 'Maria lives in Seville, in the south of Spain.' },\n { start: 4.2, end: 8.9, text: 'Every Saturday she walks to the market with her grandmother.' },\n { start: 8.9, end: 12.6, text: 'They buy oranges, fresh bread and a little cheese.' },\n { start: 12.6, end: 18.1, text: 'On the way home they stop at a café.' },\n];\n\nfunction draftsCase(\n id: string,\n about: string,\n types: AiDraftType[],\n source: AiDraftSource,\n extra: Omit<Parameters<typeof aiDraftsRequest>[0], 'types' | 'source'> = {},\n): AiCheckCase {\n const request = aiDraftsRequest({ types, source, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no drafts request for \"${id}\"`);\n }\n return { id, feature: 'draft-generation', about, request };\n}\n\n/**\n * What a host sets on every read-aloud it drafts: the SDK has no default for a\n * recording's length or the dimensions a reading is graded on.\n */\nconst READ_ALOUD_SETTINGS = {\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: { dimensions: [{ name: 'accuracy', weight: 1 }] },\n};\n\n/** Builds a case, failing loudly rather than shipping a case with no request. */\nfunction explanationCase(\n id: string,\n about: string,\n data: Parameters<typeof aiExplanationRequest>[0]['data'],\n response: LearnerResponse,\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiExplanationRequest({ data, response, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no explanation request for \"${id}\"`);\n }\n return { id, feature: 'explanation', about, request };\n}\n\nfunction hintCase(\n id: string,\n about: string,\n data: Parameters<typeof aiHintRequest>[0]['data'],\n response: LearnerResponse,\n previousHints: string[] = [],\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiHintRequest({ data, response, previousHints, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no hint request for \"${id}\"`);\n }\n return { id, feature: 'hint', about, request };\n}\n\nfunction writingCase(\n id: string,\n about: string,\n text: string,\n previousFeedback: string[] = [],\n learnerLocale?: string,\n task: { data: WrittenResponseData; group?: AiGroupInput } = { data: weekend },\n): AiCheckCase {\n const request = aiWritingFeedbackRequest({\n data: task.data,\n response: drafted(text),\n previousFeedback,\n ...(task.group ? { group: task.group } : {}),\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no writing feedback request for \"${id}\"`);\n }\n return { id, feature: 'writing-feedback', about, request };\n}\n\nfunction gradingCase(\n id: string,\n about: string,\n text: string,\n extra: { data?: WrittenResponseData; group?: AiGroupInput; learnerLocale?: string } = {},\n): AiCheckCase {\n const built = aiGradingRequest({\n data: extra.data ?? weekendExam,\n response: drafted(text),\n ...(extra.group ? { group: extra.group } : {}),\n ...(extra.learnerLocale !== undefined ? { learnerLocale: extra.learnerLocale } : {}),\n });\n if (!built.ok) {\n throw new Error(`ai-check: no grading request for \"${id}\": ${built.problem}`);\n }\n return { id, feature: 'grading', about, request: built.request };\n}\n\nfunction coachingCase(\n id: string,\n about: string,\n marks: { assessment?: SpeechAssessment; fromGradeOnly?: boolean },\n learnerLocale?: string,\n): AiCheckCase {\n const assessment = marks.assessment ?? forecast(true);\n const grade = gradeReadAloud(\n weather,\n { type: 'read-aloud', recording: { key: 'ai-check-take', mimeType: 'audio/wav' } },\n assessment,\n {\n measured: { durationMs: 4000, voicedMs: 3000 },\n plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 },\n },\n );\n if ('unscorable' in grade) {\n throw new Error(`ai-check: the reading for \"${id}\" did not grade: ${grade.reason}`);\n }\n const request = aiCoachingRequest({\n data: weather,\n ...(marks.fromGradeOnly === true ? {} : { assessment }),\n grade,\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no coaching request for \"${id}\"`);\n }\n return { id, feature: 'pronunciation-coaching', about, request };\n}\n\n/**\n * The cases {@link runAiCheck} runs: every type the SDK explains, answered\n * right, wrong and — where a type can be — partly right; every type it hints\n * for, before an answer and after a wrong one; drafts of an essay, with\n * mistakes, without, and revised; and essays to grade, on the task, with\n * mistakes to quote, off the task, and against a passage.\n *\n * They are ordinary calls. What makes them a test is that the SDK knows the\n * answer to each, so it can check what your model writes about it.\n *\n * A fresh array each call, so a caller may filter, slice or add to it without\n * changing what the next caller gets.\n */\nexport function aiCheckCases(): AiCheckCase[] {\n return [\n explanationCase('mc-correct', 'Chose the right city', capital, chose(['madrid'])),\n explanationCase('mc-wrong', 'Chose Barcelona', capital, chose(['barcelona'])),\n explanationCase('mc-blank', 'Chose nothing and submitted', capital, chose([])),\n explanationCase(\n 'fib-correct',\n 'Filled both blanks correctly',\n introduction,\n typed({ be: 'is', live: 'live' }),\n ),\n explanationCase(\n 'fib-partly',\n 'One blank right, one wrong',\n introduction,\n typed({ be: 'is', live: 'lives' }),\n ),\n explanationCase(\n 'fib-wrong',\n 'Both blanks wrong',\n introduction,\n typed({ be: 'are', live: 'living' }),\n ),\n explanationCase(\n 'gap-partly',\n 'One gap right, one wrong, in Spanish',\n landscape,\n picked({ g1: 'canon', g2: 'es' }),\n ),\n explanationCase(\n 'gap-wrong',\n 'Both gaps wrong, in Spanish',\n landscape,\n picked({ g1: 'campo', g2: 'es' }),\n ),\n explanationCase(\n 'dictation-near',\n 'Typed the sentence with two words wrong',\n listening,\n wrote('The train leaves at half past five'),\n ),\n explanationCase(\n 'dictation-correct',\n 'Typed the sentence correctly',\n listening,\n wrote('The train leaves at half past nine.'),\n ),\n explanationCase(\n 'mc-reading-wrong',\n \"Chose 'Only at full moon', on a question about its group's passage\",\n springTides,\n chose(['full-moon']),\n tides,\n ),\n hintCase('mc-hint-1', 'Has not answered yet', capital, chose([])),\n hintCase('mc-hint-2', 'Asked again after choosing Seville', capital, chose(['seville']), [\n 'Think about where the government sits.',\n ]),\n hintCase('fib-hint-1', 'Both blanks empty', introduction, typed({})),\n hintCase(\n 'fib-hint-2',\n 'Asked again with one blank wrong',\n introduction,\n typed({ be: 'are', live: '' }),\n ['Look again at the first blank.'],\n ),\n hintCase('gap-hint-1', 'Neither gap chosen, in Spanish', landscape, picked({})),\n hintCase(\n 'gap-hint-2',\n 'Asked again with the first gap wrong',\n landscape,\n picked({ g1: 'campo' }),\n ['Piensa en la forma del terreno.'],\n ),\n hintCase(\n 'mc-reading-hint',\n \"Has not answered a question whose answer its group's passage prints\",\n springTides,\n chose([]),\n [],\n tides,\n ),\n hintCase(\n 'fib-formation-hint',\n 'A word-formation blank, under the instructions its group gives',\n formation,\n typed({}),\n [],\n formationPart,\n ),\n hintCase(\n 'fib-instructions-hint',\n 'The same blank served alone, under its own instructions',\n formationAlone,\n typed({}),\n ),\n writingCase(\n 'writing-mistakes',\n 'A first draft with tense, article and agreement mistakes',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n writingCase(\n 'writing-clean',\n 'A first draft with no mistakes to correct',\n 'Last weekend I went to the market and bought an apple. My friends were there too, so we stayed until the evening. Then we went to the cinema.',\n ),\n writingCase(\n 'writing-revised',\n 'A second draft that fixed some mistakes',\n \"Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.\",\n ['Check your past tenses: \"go\" and \"buyed\" should be past forms.'],\n ),\n writingCase(\n 'writing-feedback-in-spanish',\n 'A first draft, with feedback asked for in Spanish',\n 'Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.',\n [],\n 'es',\n ),\n writingCase(\n 'writing-summary',\n \"A summary of its group's passage that gets the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n [],\n undefined,\n { data: tidesSummary, group: tides },\n ),\n coachingCase('coaching-slips', 'A reading with a mispronounced \"th\" and a word left out', {\n assessment: forecast(true),\n }),\n coachingCase('coaching-clean', 'A reading the engine marked correct throughout', {\n assessment: forecast(false),\n }),\n coachingCase(\n 'coaching-from-grade',\n 'The same slips, from a stored grade: marks without sounds',\n { fromGradeOnly: true },\n ),\n coachingCase(\n 'coaching-in-spanish',\n 'The same slips, with coaching asked for in Spanish',\n { assessment: forecast(true) },\n 'es',\n ),\n critiqueCase(\n 'critique-second-answer',\n 'A question whose distractor \"Tomato\" is botanically a fruit',\n 'multiple-choice',\n fruit,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-level',\n 'A writing task far above the A1 level it is set for',\n 'written-response',\n tooHard,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-clean',\n 'A cloze with nothing wrong in it',\n 'fill-in-the-blanks',\n cleanCloze,\n {\n level: 'A2',\n },\n ),\n critiqueCase(\n 'critique-in-spanish',\n 'The fruit question, reviewed in Spanish for its author',\n 'multiple-choice',\n fruit,\n { level: 'A1', authorLocale: 'es' },\n ),\n draftsCase(\n 'drafts-questions',\n 'Four multiple-choice questions on a short reading',\n ['multiple-choice'],\n { kind: 'passage', text: MARKET },\n { count: 4, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-cloze',\n 'A cloze on the same reading, from its transcript',\n ['fill-in-the-blanks'],\n { kind: 'transcript', text: MARKET },\n { count: 1, level: 'A2', locale: 'en', instructions: 'Blank the verbs.' },\n ),\n draftsCase(\n 'drafts-gaps',\n 'A choose-the-word item on the same reading',\n ['gap-select'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-writing',\n 'A writing task that follows on from the reading',\n ['written-response'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-video',\n 'A video quiz from timed captions: multiple choice, then dictation, then read-aloud, as many as the video is worth',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'captions', cues: MARKET_CAPTIONS },\n { level: 'A2', locale: 'en', settings: { 'read-aloud': READ_ALOUD_SETTINGS } },\n ),\n draftsCase(\n 'drafts-script',\n 'The same video from its script alone, with no times, and what the author asked for',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'transcript', text: MARKET },\n {\n level: 'A2',\n locale: 'en',\n instructions: 'Focus on what Maria and her grandmother buy.',\n settings: { 'read-aloud': READ_ALOUD_SETTINGS },\n },\n ),\n gradingCase(\n 'grading-on-task',\n 'An essay that does the task, with one slip in its tenses',\n 'Last weekend I went to the market with my sister and we bought some apples. In the afternoon we watched a film at home, and on Sunday I visit my grandmother.',\n ),\n gradingCase(\n 'grading-mistakes',\n 'An essay with tense, article and agreement mistakes, each to quote as written',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n gradingCase(\n 'grading-off-task',\n 'An essay about something other than last weekend',\n 'My favourite food is pizza. I like it with cheese and tomato, and I eat it every Friday with my family at a small restaurant near our house.',\n ),\n gradingCase(\n 'grading-summary',\n \"A summary graded against its group's passage, getting the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n { data: tidesSummaryExam, group: tidesExam },\n ),\n gradingCase(\n 'grading-in-spanish',\n 'The essay with mistakes, with comments asked for in Spanish',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n { learnerLocale: 'es' },\n ),\n ];\n}\n","/**\n * A test kit for the prompts behind your AI ports.\n *\n * The SDK cannot test your model, and will not try. What it can do is make the\n * calls a learner's questions would make — on items whose answers it knows —\n * and run the same checks it runs before a learner sees anything. What comes\n * back is a count of what would have been shown and what would have been\n * refused, and why.\n *\n * Run it where you change a prompt or a model, in your own CI, with your own\n * key. It is a separate entry point (`@intellectif/lk-core/ai-check`) because\n * it is for your test run, not for a learner's browser: nothing here is\n * imported by anything a page renders.\n *\n * ```ts\n * import { aiCheckCases, formatAiCheckReport, runAiCheck } from '@intellectif/lk-core/ai-check';\n *\n * const report = await runAiCheck({\n * explain: (request) => callMyModel(EXPLAIN_PROMPT, request),\n * hint: (request) => callMyModel(HINT_PROMPT, request),\n * writingFeedback: (request) => callMyModel(FEEDBACK_PROMPT, request),\n * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),\n * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),\n * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),\n * grading: (request) => callMyModel(GRADING_PROMPT, request),\n * });\n * console.log(formatAiCheckReport(report));\n * if (report.refused > 0 || report.errors > 0) {\n * process.exitCode = 1;\n * }\n * ```\n *\n * A refusal is a real failure, not a warning: it is an answer a learner would\n * have asked for and not received. `reveals-answer` on a hint case is the one\n * to take most seriously — that prompt gives answers away. Next come\n * `misquotes-answer` on a writing case — that prompt corrects words the learner\n * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches\n * words the engine heard as right, or sounds it never reported. On a grading\n * case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one\n * that leaves a criterion out grades a different task, and one off a\n * criterion's scale grades on a scale the author did not write.\n *\n * A grading case the SDK accepts is a reply of the right shape, not a right\n * grade: whether a model grades as your teachers do is what a calibration\n * measures (`calibrationReport`), on your own responses.\n */\nimport { checkAiExplanation, checkAiHint } from '../ai.js';\nimport { checkAiCoaching } from '../ai-coaching.js';\nimport { checkAiCritique } from '../ai-critique.js';\nimport { checkAiDrafts } from '../ai-drafts.js';\nimport { checkAiGrading } from '../ai-grading.js';\nimport { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\n AiCritique,\n AiCritiqueRequest,\n AiDrafts,\n AiDraftsRequest,\n AiExplanationRequest,\n AiGradingJudgement,\n AiGradingRequest,\n AiHintRequest,\n AiRefusal,\n AiTextResult,\n AiWritingFeedback,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport { type AiCheckCase, aiCheckCases } from './cases.js';\n\nexport { type AiCheckCase, aiCheckCases } from './cases.js';\n\n/**\n * Your ports, as this kit calls them. The same functions\n * `<LkAiProvider ai={…}>` takes, minus the `AbortSignal` a browser passes —\n * nothing here abandons a call.\n *\n * Leave one out and its cases are skipped rather than failed.\n */\nexport interface AiCheckPorts {\n explain?(request: AiExplanationRequest): Promise<unknown> | unknown;\n hint?(request: AiHintRequest): Promise<unknown> | unknown;\n writingFeedback?(request: AiWritingFeedbackRequest): Promise<unknown> | unknown;\n pronunciationCoaching?(request: AiCoachingRequest): Promise<unknown> | unknown;\n /** Reviews an item for its author: the port an editor's \"Review with AI\" calls. */\n critique?(request: AiCritiqueRequest): Promise<unknown> | unknown;\n /** Drafts items from a source, for an author to approve. */\n drafts?(request: AiDraftsRequest): Promise<unknown> | unknown;\n /** Grades a submitted essay on its rubric: a server's port, which no component calls. */\n grading?(request: AiGradingRequest): Promise<unknown> | unknown;\n}\n\n/** What happened on one case. */\nexport interface AiCheckResult {\n case: AiCheckCase;\n /** Whether a learner would have been shown what came back. */\n ok: boolean;\n /** Why it would not have been shown. */\n refusal?: AiRefusal;\n /** The port threw or rejected. Its message, as a string. */\n error?: string;\n /**\n * What came back, once the SDK had read it — so a refusal can be read beside\n * the text that caused it. Absent when the port failed or answered with\n * nothing the SDK could read.\n */\n result?: AiTextResult;\n /** For a writing case a learner would have been shown: the feedback, as the SDK accepted it. */\n feedback?: AiWritingFeedback;\n /** For a coaching case a learner would have been shown: the coaching, as the SDK accepted it. */\n coaching?: AiCoaching;\n /** For a critique case an author would have been shown: the findings, as the SDK accepted them. */\n critique?: AiCritique;\n /**\n * For a drafts case the SDK read: the drafts, each with what `validateDraft`\n * and the item critic say of it. A reply read is not a good one — check how\n * many came back `complete` and without warnings.\n */\n drafts?: AiDrafts;\n /**\n * For a grading case the SDK accepted: the judgement, as checked. A reply of\n * the right shape, not a right grade — `gradeFromJudgement` turns it into one.\n */\n grading?: AiGradingJudgement;\n /** How long the call took, in milliseconds. */\n ms: number;\n}\n\n/** What a run came to. */\nexport interface AiCheckReport {\n /** Cases run: the ones a port was supplied for. */\n total: number;\n /** Cases whose answer a learner would have been shown. */\n shown: number;\n /** Cases whose answer the SDK refused. */\n refused: number;\n /** Cases where the port itself failed. */\n errors: number;\n /** Cases skipped because no port was supplied for that feature. */\n skipped: number;\n /** How many refusals of each reason. */\n byRefusal: Record<AiRefusal, number>;\n results: AiCheckResult[];\n}\n\nexport interface AiCheckOptions {\n /**\n * The cases to run. Defaults to {@link aiCheckCases}. Pass a filtered list to\n * run one type, or your own cases — built with `aiExplanationRequest` and\n * `aiHintRequest` on your own items, which is how you test a prompt against\n * the content your learners actually see.\n */\n cases?: readonly AiCheckCase[];\n /**\n * How many cases to have in flight at once. Default 1: a run in CI hits a\n * rate limit far more often than it runs out of patience.\n */\n concurrency?: number;\n}\n\n/**\n * Every refusal, in the order a report lists them. A map rather than a list, so\n * a refusal added to `AiRefusal` and not here fails to compile instead of going\n * uncounted.\n */\nconst REFUSAL_ORDER = {\n malformed: true,\n empty: true,\n 'too-long': true,\n 'contradicts-grade': true,\n 'reveals-answer': true,\n 'misquotes-answer': true,\n 'contradicts-marks': true,\n 'contradicts-item': true,\n 'misses-criteria': true,\n 'out-of-scale': true,\n} as const satisfies Record<AiRefusal, true>;\n\nconst REFUSALS = Object.keys(REFUSAL_ORDER) as AiRefusal[];\n\n/** The port a case is for. */\nfunction portFor(\n ports: AiCheckPorts,\n one: AiCheckCase,\n): ((request: never) => Promise<unknown> | unknown) | undefined {\n switch (one.request.feature) {\n case 'explanation':\n return ports.explain as never;\n case 'hint':\n return ports.hint as never;\n case 'pronunciation-coaching':\n return ports.pronunciationCoaching as never;\n case 'item-critique':\n return ports.critique as never;\n case 'draft-generation':\n return ports.drafts as never;\n case 'grading':\n return ports.grading as never;\n default:\n return ports.writingFeedback as never;\n }\n}\n\n/** Milliseconds, from whichever clock this runtime has. */\nconst now = (): number => Date.now();\n\n/** Runs one case: calls the port, times it, and checks what came back. */\nasync function runCase(ports: AiCheckPorts, one: AiCheckCase): Promise<AiCheckResult> {\n const started = now();\n const port = portFor(ports, one);\n if (port === undefined) {\n throw new Error(`ai-check: no port for \"${one.id}\"`);\n }\n let raw: unknown;\n try {\n raw = await port(one.request as never);\n } catch (error) {\n return {\n case: one,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n ms: now() - started,\n };\n }\n const ms = now() - started;\n if (one.request.feature === 'grading') {\n const checked = checkAiGrading(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, grading: checked.judgement, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'draft-generation') {\n let next = 0;\n const checked = checkAiDrafts(raw, one.request, {\n newId: () => {\n next += 1;\n return `ai-check-${next}`;\n },\n });\n return checked.ok\n ? { case: one, ok: true, drafts: checked.drafts, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'item-critique') {\n const checked = checkAiCritique(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, critique: checked.critique, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'pronunciation-coaching') {\n const checked = checkAiCoaching(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.coaching;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n coaching: checked.coaching,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'writing-feedback') {\n const checked = checkAiWritingFeedback(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.feedback;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n feedback: checked.feedback,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n const checked =\n one.request.feature === 'explanation'\n ? checkAiExplanation(raw, one.request)\n : checkAiHint(raw, one.request);\n if (checked.ok) {\n return { case: one, ok: true, result: checked.result, ms };\n }\n return refused(one, raw, checked.refusal, ms);\n}\n\n/** A case whose answer was refused, with the text that came back when there was one to read. */\nfunction refused(one: AiCheckCase, raw: unknown, refusal: AiRefusal, ms: number): AiCheckResult {\n const text =\n typeof (raw as { text?: unknown })?.text === 'string' ? (raw as AiTextResult) : undefined;\n return {\n case: one,\n ok: false,\n refusal,\n ...(text !== undefined ? { result: text } : {}),\n ms,\n };\n}\n\n/**\n * Calls your ports on every case and reports what a learner would have seen.\n *\n * It never throws for a failing case: a port that rejects is an `error` in the\n * report, so one bad call cannot hide the rest.\n */\nexport async function runAiCheck(\n ports: AiCheckPorts,\n options: AiCheckOptions = {},\n): Promise<AiCheckReport> {\n const all = options.cases ?? aiCheckCases();\n const asked = Math.trunc(options.concurrency ?? 1);\n const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;\n const runnable = all.filter((one) => portFor(ports, one) !== undefined);\n const results: AiCheckResult[] = new Array(runnable.length);\n let next = 0;\n const lane = async (): Promise<void> => {\n while (next < runnable.length) {\n const mine = next;\n next += 1;\n const one = runnable[mine];\n if (one === undefined) {\n return;\n }\n results[mine] = await runCase(ports, one);\n }\n };\n await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));\n\n const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0])) as Record<\n AiRefusal,\n number\n >;\n let shown = 0;\n let refused = 0;\n let errors = 0;\n for (const result of results) {\n if (result.ok) {\n shown += 1;\n } else if (result.refusal !== undefined) {\n refused += 1;\n byRefusal[result.refusal] += 1;\n } else {\n errors += 1;\n }\n }\n return {\n total: results.length,\n shown,\n refused,\n errors,\n skipped: all.length - runnable.length,\n byRefusal,\n results,\n };\n}\n\n/**\n * The report as a few lines for a CI log: the counts, then every case that\n * failed, with what came back.\n *\n * The refused text is printed — this is your own test run, not a learner's\n * screen, and a hint that gives the answer away is only fixable if you can\n * read it.\n */\nexport function formatAiCheckReport(report: AiCheckReport): string {\n const lines: string[] = [];\n const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);\n lines.push(\n `ai-check: ${report.shown}/${report.total} shown, ${report.refused} refused, ${report.errors} failed` +\n `${report.skipped > 0 ? `, ${report.skipped} skipped (no port)` : ''} — slowest ${slowest} ms`,\n );\n for (const reason of REFUSALS) {\n const count = report.byRefusal[reason];\n if (count > 0) {\n lines.push(` ${reason}: ${count}`);\n }\n }\n for (const result of report.results) {\n if (result.ok) {\n continue;\n }\n const why = result.refusal ?? `port failed (${result.error ?? 'no message'})`;\n lines.push(` ✗ ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);\n if (result.result !== undefined) {\n lines.push(` ${result.result.text.replace(/\\s+/g, ' ').slice(0, 200)}`);\n }\n }\n return lines.join('\\n');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,UAA8B;CAClC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;CACV,MAAM;CACN,iBAAiB;CACjB,SAAS;EACP;GAAE,IAAI;GAAU,MAAM;GAAU,WAAW;EAAK;EAChD;GAAE,IAAI;GAAa,MAAM;GAAa,WAAW;EAAM;EACvD;GAAE,IAAI;GAAW,MAAM;GAAW,WAAW;EAAM;CACrD;AACF;AAEA,MAAM,eAAoC;CACxC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,SAAS;CACT,QAAQ,CACN;EAAE,IAAI;EAAM,iBAAiB,CAAC,IAAI;CAAE,GACpC;EAAE,IAAI;EAAQ,iBAAiB,CAAC,MAAM;EAAG,MAAM;CAAgC,CACjF;CACA,iBAAiB;AACnB;AAEA,MAAM,YAA2B;CAC/B,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,SAAS;CACT,MAAM,CACJ;EACE,IAAI;EACJ,SAAS,CACP;GAAE,IAAI;GAAS,MAAM;EAAQ,GAC7B;GAAE,IAAI;GAAS,MAAM;EAAQ,CAC/B;EACA,iBAAiB;CACnB,GACA;EACE,IAAI;EACJ,SAAS,CACP;GAAE,IAAI;GAAQ,MAAM;EAAO,GAC3B;GAAE,IAAI;GAAM,MAAM;EAAK,CACzB;EACA,iBAAiB;CACnB,CACF;CACA,iBAAiB;AACnB;AAEA,MAAM,YAA2B;CAC/B,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,YAAY;AACd;;;;;;AAOA,MAAM,QACJ;AAIF,MAAM,cAAkC;CACtC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;CACV,MAAM;CACN,iBAAiB;CACjB,SAAS;EACP;GAAE,IAAI;GAAW,MAAM;GAAqC,WAAW;EAAK;EAC5E;GAAE,IAAI;GAAa,MAAM;GAAqB,WAAW;EAAM;EAC/D;GAAE,IAAI;GAAU,MAAM;GAAa,WAAW;EAAM;CACtD;AACF;AAEA,MAAM,eAAoC;CACxC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACV,gBAAgB;AAClB;AAEA,MAAM,QAAmB;CACvB,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;EAAE,IAAI;EAAuB,MAAM;EAAQ,OAAO;EAAS,MAAM;CAAM;CACjF,OAAO,CAAC,aAAa,YAAY;AACnC;;AAGA,MAAM,YAAiC;CACrC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,SAAS;CACT,QAAQ,CAAC;EAAE,IAAI;EAAK,iBAAiB,CAAC,SAAS;CAAE,CAAC;CAClD,iBAAiB;AACnB;AAEA,MAAM,gBAA2B;CAC/B,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;EACR,IAAI;EACJ,MAAM;EACN,MAAM;CACR;CACA,OAAO,CAAC,SAAS;AACnB;;AAGA,MAAM,iBAAsC;CAC1C,GAAG;CACH,IAAI;CACJ,cACE;AACJ;AAEA,MAAM,UAA+B;CACnC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACV,gBAAgB;CAChB,QAAQ,EACN,UAAU;EACR;GAAE,MAAM;GAAW,aAAa;GAAoC,QAAQ;EAAE;EAC9E;GAAE,MAAM;GAAc,aAAa;GAAkC,QAAQ;EAAE;EAC/E;GAAE,MAAM;GAAQ,aAAa;GAAuC,QAAQ;EAAE;CAChF,EACF;AACF;;;;;;AAOA,MAAM,cAAmC;CACvC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACV,gBAAgB;CAChB,QAAQ,EACN,UAAU;EACR;GACE,MAAM;GACN,aAAa;GACb,QAAQ;GACR,QAAQ;IACN;KAAE,OAAO;KAAW,aAAa;IAAyB;IAC1D;KAAE,OAAO;KAAc,aAAa;IAA8C;IAClF;KAAE,OAAO;KAAO,aAAa;IAA+B;GAC9D;EACF;EACA;GACE,MAAM;GACN,aAAa;GACb,QAAQ;GACR,QAAQ;IACN;KAAE,OAAO;KAAW,aAAa;IAAkD;IACnF;KAAE,OAAO;KAAS,aAAa;IAA8C;IAC7E;KAAE,OAAO;KAAQ,aAAa;IAAwC;IACtE;KACE,OAAO;KACP,aAAa;IACf;GACF;EACF;EACA;GAAE,MAAM;GAAc,aAAa;GAAkC,QAAQ;EAAE;CACjF,EACF;AACF;;AAGA,MAAM,mBAAwC;CAC5C,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACV,gBAAgB;CAChB,QAAQ,EACN,UAAU,CACR;EACE,MAAM;EACN,aAAa;EACb,QAAQ;EACR,QAAQ;GACN;IAAE,OAAO;IAAS,aAAa;GAA+B;GAC9D;IAAE,OAAO;IAAgB,aAAa;GAAiD;GACvF;IAAE,OAAO;IAAS,aAAa;GAAqC;EACtE;CACF,GACA;EAAE,MAAM;EAAY,aAAa;EAA+C,QAAQ;CAAE,CAC5F,EACF;AACF;AAEA,MAAM,YAAuB;CAC3B,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;EAAE,IAAI;EAA4B,MAAM;EAAQ,OAAO;EAAS,MAAM;CAAM;CACtF,OAAO,CAAC,aAAa,gBAAgB;AACvC;AAEA,MAAM,UAAyB;CAC7B,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,eAAe;CACf,QAAQ;CACR,WAAW,EAAE,YAAY,GAAG;CAC5B,SAAS,EACP,YAAY,CACV;EAAE,MAAM;EAAY,QAAQ;CAAE,GAC9B;EAAE,MAAM;EAAW,QAAQ;CAAE,CAC/B,EACF;AACF;;AAGA,MAAM,QAAQ,MAAc,OAA4B,CAAC,OAAmB;CAC1E;CACA,OAAO;CACP,UAAU;CACV,GAAG;AACL;;;;;;AAOA,SAAS,SAAS,OAAkC;CAClD,OAAO;EACL,mBAAmB;EACnB,QAAQ;EACR,MAAM;EACN,QAAQ;EACR,eAAe,QAAQ;EACvB,cAAc;EACd,UAAU;GAAE,MAAM;GAAQ,IAAI;EAAuB;EACrD,OAAO;EACP,QAAQ,QAAQ;GAAE,UAAU;GAAI,SAAS;GAAI,cAAc;EAAG,IAAI;GAAE,UAAU;GAAI,SAAS;EAAG;EAC9F,QAAQ;EACR,iBAAiB;EACjB,OAAO;GACL,KAAK,KAAK;GACV,QACI,KAAK,WAAW;IACd,UAAU;IACV,OAAO;IACP,UAAU;KACR;MAAE,QAAQ;MAAK,UAAU;KAAG;KAC5B;MAAE,QAAQ;MAAK,UAAU;KAAG;KAC5B;MACE,QAAQ;MACR,UAAU;MACV,SAAS;OACP;QAAE,QAAQ;QAAK,OAAO;OAAG;OACzB;QAAE,QAAQ;QAAK,OAAO;OAAG;OACzB;QAAE,QAAQ;QAAK,OAAO;OAAG;MAC3B;KACF;KACA;MAAE,QAAQ;MAAK,UAAU;KAAG;IAC9B;GACF,CAAC,IACD,KAAK,SAAS;GAClB,KAAK,IAAI;GACT,KAAK,MAAM;GACX,KAAK,IAAI;GACT,KAAK,KAAK;GACV,QAAQ;IAAE,MAAM;IAAS,OAAO;GAAW,IAAI,KAAK,OAAO;EAC7D;CACF;AACF;AA+BA,MAAM,SAAS,SAAoC;CACjD,MAAM;CACN,mBAAmB;AACrB;AACA,MAAM,SAAS,aAAsD;CACnE,MAAM;CACN;AACF;AACA,MAAM,UAAU,aAAsD;CACpE,MAAM;CACN,YAAY;AACd;AACA,MAAM,SAAS,UAAmC;CAAE,MAAM;CAAa;AAAK;AAC5E,MAAM,WAAW,UAAmC;CAClD,MAAM;CACN;CACA,WAAW;AACb;;;;;;AAOA,MAAM,QAA4B;CAChC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,UAAU;CACV,MAAM;CACN,iBAAiB;CACjB,SAAS;EACP;GAAE,IAAI;GAAM,MAAM;GAAS,WAAW;EAAK;EAC3C;GAAE,IAAI;GAAM,MAAM;GAAU,WAAW;EAAM;EAC7C;GAAE,IAAI;GAAM,MAAM;GAAU,WAAW;EAAM;CAC/C;AACF;AAEA,MAAM,UAA+B;CACnC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;AACZ;AAEA,MAAM,aAAkC;CACtC,eAAe;CACf,MAAM;CACN,IAAI;CACJ,OAAO;CACP,SAAS;CACT,QAAQ,CACN;EAAE,IAAI;EAAK,iBAAiB,CAAC,OAAO,MAAM;CAAE,GAC5C;EAAE,IAAI;EAAK,iBAAiB,CAAC,QAAQ,MAAM;CAAE,CAC/C;CACA,iBAAiB;AACnB;AAEA,SAAS,aACP,IACA,OACA,MACA,OACA,QAAmD,CAAC,GACvC;CACb,MAAM,UAAU,kBAAkB;EAAE;EAAM;EAAO,GAAG;CAAM,CAAC;CAC3D,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,sCAAsC,GAAG,EAAE;CAE7D,OAAO;EAAE;EAAI,SAAS;EAAiB;EAAO;CAAQ;AACxD;;AAGA,MAAM,SACJ;;AAKF,MAAM,kBAAkB;CACtB;EAAE,OAAO;EAAG,KAAK;EAAK,MAAM;CAAiD;CAC7E;EAAE,OAAO;EAAK,KAAK;EAAK,MAAM;CAA+D;CAC7F;EAAE,OAAO;EAAK,KAAK;EAAM,MAAM;CAAqD;CACpF;EAAE,OAAO;EAAM,KAAK;EAAM,MAAM;CAAuC;AACzE;AAEA,SAAS,WACP,IACA,OACA,OACA,QACA,QAAyE,CAAC,GAC7D;CACb,MAAM,UAAU,gBAAgB;EAAE;EAAO;EAAQ,GAAG;CAAM,CAAC;CAC3D,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,oCAAoC,GAAG,EAAE;CAE3D,OAAO;EAAE;EAAI,SAAS;EAAoB;EAAO;CAAQ;AAC3D;;;;;AAMA,MAAM,sBAAsB;CAC1B,QAAQ;CACR,WAAW,EAAE,YAAY,GAAG;CAC5B,SAAS,EAAE,YAAY,CAAC;EAAE,MAAM;EAAY,QAAQ;CAAE,CAAC,EAAE;AAC3D;;AAGA,SAAS,gBACP,IACA,OACA,MACA,UACA,OACa;CACb,MAAM,UAAU,qBAAqB;EAAE;EAAM;EAAU,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;CAAG,CAAC;CACpF,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,yCAAyC,GAAG,EAAE;CAEhE,OAAO;EAAE;EAAI,SAAS;EAAe;EAAO;CAAQ;AACtD;AAEA,SAAS,SACP,IACA,OACA,MACA,UACA,gBAA0B,CAAC,GAC3B,OACa;CACb,MAAM,UAAU,cAAc;EAAE;EAAM;EAAU;EAAe,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;CAAG,CAAC;CAC5F,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,kCAAkC,GAAG,EAAE;CAEzD,OAAO;EAAE;EAAI,SAAS;EAAQ;EAAO;CAAQ;AAC/C;AAEA,SAAS,YACP,IACA,OACA,MACA,mBAA6B,CAAC,GAC9B,eACA,OAA4D,EAAE,MAAM,QAAQ,GAC/D;CACb,MAAM,UAAU,yBAAyB;EACvC,MAAM,KAAK;EACX,UAAU,QAAQ,IAAI;EACtB;EACA,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EAC1C,GAAI,kBAAkB,KAAA,IAAY,EAAE,cAAc,IAAI,CAAC;CACzD,CAAC;CACD,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,8CAA8C,GAAG,EAAE;CAErE,OAAO;EAAE;EAAI,SAAS;EAAoB;EAAO;CAAQ;AAC3D;AAEA,SAAS,YACP,IACA,OACA,MACA,QAAsF,CAAC,GAC1E;CACb,MAAM,QAAQ,iBAAiB;EAC7B,MAAM,MAAM,QAAQ;EACpB,UAAU,QAAQ,IAAI;EACtB,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;EAC5C,GAAI,MAAM,kBAAkB,KAAA,IAAY,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;CACpF,CAAC;CACD,IAAI,CAAC,MAAM,IACT,MAAM,IAAI,MAAM,qCAAqC,GAAG,KAAK,MAAM,SAAS;CAE9E,OAAO;EAAE;EAAI,SAAS;EAAW;EAAO,SAAS,MAAM;CAAQ;AACjE;AAEA,SAAS,aACP,IACA,OACA,OACA,eACa;CACb,MAAM,aAAa,MAAM,cAAc,SAAS,IAAI;CACpD,MAAM,QAAQ,eACZ,SACA;EAAE,MAAM;EAAc,WAAW;GAAE,KAAK;GAAiB,UAAU;EAAY;CAAE,GACjF,YACA;EACE,UAAU;GAAE,YAAY;GAAM,UAAU;EAAK;EAC7C,cAAc;GAAE,mBAAmB;GAAG,aAAa;EAAI;CACzD,CACF;CACA,IAAI,gBAAgB,OAClB,MAAM,IAAI,MAAM,8BAA8B,GAAG,mBAAmB,MAAM,QAAQ;CAEpF,MAAM,UAAU,kBAAkB;EAChC,MAAM;EACN,GAAI,MAAM,kBAAkB,OAAO,CAAC,IAAI,EAAE,WAAW;EACrD;EACA,GAAI,kBAAkB,KAAA,IAAY,EAAE,cAAc,IAAI,CAAC;CACzD,CAAC;CACD,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,sCAAsC,GAAG,EAAE;CAE7D,OAAO;EAAE;EAAI,SAAS;EAA0B;EAAO;CAAQ;AACjE;;;;;;;;;;;;;;AAeA,SAAgB,eAA8B;CAC5C,OAAO;EACL,gBAAgB,cAAc,wBAAwB,SAAS,MAAM,CAAC,QAAQ,CAAC,CAAC;EAChF,gBAAgB,YAAY,mBAAmB,SAAS,MAAM,CAAC,WAAW,CAAC,CAAC;EAC5E,gBAAgB,YAAY,+BAA+B,SAAS,MAAM,CAAC,CAAC,CAAC;EAC7E,gBACE,eACA,gCACA,cACA,MAAM;GAAE,IAAI;GAAM,MAAM;EAAO,CAAC,CAClC;EACA,gBACE,cACA,8BACA,cACA,MAAM;GAAE,IAAI;GAAM,MAAM;EAAQ,CAAC,CACnC;EACA,gBACE,aACA,qBACA,cACA,MAAM;GAAE,IAAI;GAAO,MAAM;EAAS,CAAC,CACrC;EACA,gBACE,cACA,wCACA,WACA,OAAO;GAAE,IAAI;GAAS,IAAI;EAAK,CAAC,CAClC;EACA,gBACE,aACA,+BACA,WACA,OAAO;GAAE,IAAI;GAAS,IAAI;EAAK,CAAC,CAClC;EACA,gBACE,kBACA,2CACA,WACA,MAAM,oCAAoC,CAC5C;EACA,gBACE,qBACA,gCACA,WACA,MAAM,qCAAqC,CAC7C;EACA,gBACE,oBACA,sEACA,aACA,MAAM,CAAC,WAAW,CAAC,GACnB,KACF;EACA,SAAS,aAAa,wBAAwB,SAAS,MAAM,CAAC,CAAC,CAAC;EAChE,SAAS,aAAa,sCAAsC,SAAS,MAAM,CAAC,SAAS,CAAC,GAAG,CACvF,wCACF,CAAC;EACD,SAAS,cAAc,qBAAqB,cAAc,MAAM,CAAC,CAAC,CAAC;EACnE,SACE,cACA,oCACA,cACA,MAAM;GAAE,IAAI;GAAO,MAAM;EAAG,CAAC,GAC7B,CAAC,gCAAgC,CACnC;EACA,SAAS,cAAc,kCAAkC,WAAW,OAAO,CAAC,CAAC,CAAC;EAC9E,SACE,cACA,wCACA,WACA,OAAO,EAAE,IAAI,QAAQ,CAAC,GACtB,CAAC,iCAAiC,CACpC;EACA,SACE,mBACA,uEACA,aACA,MAAM,CAAC,CAAC,GACR,CAAC,GACD,KACF;EACA,SACE,sBACA,kEACA,WACA,MAAM,CAAC,CAAC,GACR,CAAC,GACD,aACF;EACA,SACE,yBACA,2DACA,gBACA,MAAM,CAAC,CAAC,CACV;EACA,YACE,oBACA,4DACA,kIACF;EACA,YACE,iBACA,6CACA,+IACF;EACA,YACE,mBACA,2CACA,+GACA,CAAC,oEAAgE,CACnE;EACA,YACE,+BACA,qDACA,gGACA,CAAC,GACD,IACF;EACA,YACE,mBACA,qEACA,6GACA,CAAC,GACD,KAAA,GACA;GAAE,MAAM;GAAc,OAAO;EAAM,CACrC;EACA,aAAa,kBAAkB,6DAA2D,EACxF,YAAY,SAAS,IAAI,EAC3B,CAAC;EACD,aAAa,kBAAkB,kDAAkD,EAC/E,YAAY,SAAS,KAAK,EAC5B,CAAC;EACD,aACE,uBACA,6DACA,EAAE,eAAe,KAAK,CACxB;EACA,aACE,uBACA,sDACA,EAAE,YAAY,SAAS,IAAI,EAAE,GAC7B,IACF;EACA,aACE,0BACA,iEACA,mBACA,OACA,EAAE,OAAO,KAAK,CAChB;EACA,aACE,kBACA,uDACA,oBACA,SACA,EAAE,OAAO,KAAK,CAChB;EACA,aACE,kBACA,oCACA,sBACA,YACA,EACE,OAAO,KACT,CACF;EACA,aACE,uBACA,0DACA,mBACA,OACA;GAAE,OAAO;GAAM,cAAc;EAAK,CACpC;EACA,WACE,oBACA,qDACA,CAAC,iBAAiB,GAClB;GAAE,MAAM;GAAW,MAAM;EAAO,GAChC;GAAE,OAAO;GAAG,OAAO;GAAM,QAAQ;EAAK,CACxC;EACA,WACE,gBACA,oDACA,CAAC,oBAAoB,GACrB;GAAE,MAAM;GAAc,MAAM;EAAO,GACnC;GAAE,OAAO;GAAG,OAAO;GAAM,QAAQ;GAAM,cAAc;EAAmB,CAC1E;EACA,WACE,eACA,8CACA,CAAC,YAAY,GACb;GAAE,MAAM;GAAW,MAAM;EAAO,GAChC;GAAE,OAAO;GAAG,OAAO;GAAM,QAAQ;EAAK,CACxC;EACA,WACE,kBACA,mDACA,CAAC,kBAAkB,GACnB;GAAE,MAAM;GAAW,MAAM;EAAO,GAChC;GAAE,OAAO;GAAG,OAAO;GAAM,QAAQ;EAAK,CACxC;EACA,WACE,gBACA,qHACA;GAAC;GAAmB;GAAa;EAAY,GAC7C;GAAE,MAAM;GAAY,MAAM;EAAgB,GAC1C;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,EAAE,cAAc,oBAAoB;EAAE,CAC/E;EACA,WACE,iBACA,sFACA;GAAC;GAAmB;GAAa;EAAY,GAC7C;GAAE,MAAM;GAAc,MAAM;EAAO,GACnC;GACE,OAAO;GACP,QAAQ;GACR,cAAc;GACd,UAAU,EAAE,cAAc,oBAAoB;EAChD,CACF;EACA,YACE,mBACA,4DACA,+JACF;EACA,YACE,oBACA,iFACA,kIACF;EACA,YACE,oBACA,oDACA,8IACF;EACA,YACE,mBACA,gFACA,6GACA;GAAE,MAAM;GAAkB,OAAO;EAAU,CAC7C;EACA,YACE,sBACA,+DACA,oIACA,EAAE,eAAe,KAAK,CACxB;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtrBA,MAAM,WAAW,OAAO,KAAK;CAZ3B,WAAW;CACX,OAAO;CACP,YAAY;CACZ,qBAAqB;CACrB,kBAAkB;CAClB,oBAAoB;CACpB,qBAAqB;CACrB,oBAAoB;CACpB,mBAAmB;CACnB,gBAAgB;AAGuB,CAAC;;AAG1C,SAAS,QACP,OACA,KAC8D;CAC9D,QAAQ,IAAI,QAAQ,SAApB;EACE,KAAK,eACH,OAAO,MAAM;EACf,KAAK,QACH,OAAO,MAAM;EACf,KAAK,0BACH,OAAO,MAAM;EACf,KAAK,iBACH,OAAO,MAAM;EACf,KAAK,oBACH,OAAO,MAAM;EACf,KAAK,WACH,OAAO,MAAM;EACf,SACE,OAAO,MAAM;CACjB;AACF;;AAGA,MAAM,YAAoB,KAAK,IAAI;;AAGnC,eAAe,QAAQ,OAAqB,KAA0C;CACpF,MAAM,UAAU,IAAI;CACpB,MAAM,OAAO,QAAQ,OAAO,GAAG;CAC/B,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,0BAA0B,IAAI,GAAG,EAAE;CAErD,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,KAAK,IAAI,OAAgB;CACvC,SAAS,OAAO;EACd,OAAO;GACL,MAAM;GACN,IAAI;GACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC5D,IAAI,IAAI,IAAI;EACd;CACF;CACA,MAAM,KAAK,IAAI,IAAI;CACnB,IAAI,IAAI,QAAQ,YAAY,WAAW;EACrC,MAAM,UAAU,eAAe,KAAK,IAAI,OAAO;EAC/C,OAAO,QAAQ,KACX;GAAE,MAAM;GAAK,IAAI;GAAM,SAAS,QAAQ;GAAW;EAAG,IACtD,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;CAC3C;CACA,IAAI,IAAI,QAAQ,YAAY,oBAAoB;EAC9C,IAAI,OAAO;EACX,MAAM,UAAU,cAAc,KAAK,IAAI,SAAS,EAC9C,aAAa;GACX,QAAQ;GACR,OAAO,YAAY;EACrB,EACF,CAAC;EACD,OAAO,QAAQ,KACX;GAAE,MAAM;GAAK,IAAI;GAAM,QAAQ,QAAQ;GAAQ;EAAG,IAClD,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;CAC3C;CACA,IAAI,IAAI,QAAQ,YAAY,iBAAiB;EAC3C,MAAM,UAAU,gBAAgB,KAAK,IAAI,OAAO;EAChD,OAAO,QAAQ,KACX;GAAE,MAAM;GAAK,IAAI;GAAM,UAAU,QAAQ;GAAU;EAAG,IACtD,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;CAC3C;CACA,IAAI,IAAI,QAAQ,YAAY,0BAA0B;EACpD,MAAM,UAAU,gBAAgB,KAAK,IAAI,OAAO;EAChD,IAAI,QAAQ,IAAI;GACd,MAAM,EAAE,MAAM,YAAY,UAAU,QAAQ;GAC5C,OAAO;IACL,MAAM;IACN,IAAI;IACJ,QAAQ;KACN;KACA,GAAI,eAAe,KAAA,IAAY,EAAE,WAAW,IAAI,CAAC;KACjD,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;IACzC;IACA,UAAU,QAAQ;IAClB;GACF;EACF;EACA,OAAO,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;CAC9C;CACA,IAAI,IAAI,QAAQ,YAAY,oBAAoB;EAC9C,MAAM,UAAU,uBAAuB,KAAK,IAAI,OAAO;EACvD,IAAI,QAAQ,IAAI;GACd,MAAM,EAAE,MAAM,YAAY,UAAU,QAAQ;GAC5C,OAAO;IACL,MAAM;IACN,IAAI;IACJ,QAAQ;KACN;KACA,GAAI,eAAe,KAAA,IAAY,EAAE,WAAW,IAAI,CAAC;KACjD,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;IACzC;IACA,UAAU,QAAQ;IAClB;GACF;EACF;EACA,OAAO,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;CAC9C;CACA,MAAM,UACJ,IAAI,QAAQ,YAAY,gBACpB,mBAAmB,KAAK,IAAI,OAAO,IACnC,YAAY,KAAK,IAAI,OAAO;CAClC,IAAI,QAAQ,IACV,OAAO;EAAE,MAAM;EAAK,IAAI;EAAM,QAAQ,QAAQ;EAAQ;CAAG;CAE3D,OAAO,QAAQ,KAAK,KAAK,QAAQ,SAAS,EAAE;AAC9C;;AAGA,SAAS,QAAQ,KAAkB,KAAc,SAAoB,IAA2B;CAC9F,MAAM,OACJ,OAAQ,KAA4B,SAAS,WAAY,MAAuB,KAAA;CAClF,OAAO;EACL,MAAM;EACN,IAAI;EACJ;EACA,GAAI,SAAS,KAAA,IAAY,EAAE,QAAQ,KAAK,IAAI,CAAC;EAC7C;CACF;AACF;;;;;;;AAQA,eAAsB,WACpB,OACA,UAA0B,CAAC,GACH;CACxB,MAAM,MAAM,QAAQ,SAAS,aAAa;CAC1C,MAAM,QAAQ,KAAK,MAAM,QAAQ,eAAe,CAAC;CACjD,MAAM,QAAQ,OAAO,SAAS,KAAK,KAAK,SAAS,IAAI,KAAK,IAAI,OAAO,EAAE,IAAI;CAC3E,MAAM,WAAW,IAAI,QAAQ,QAAQ,QAAQ,OAAO,GAAG,MAAM,KAAA,CAAS;CACtE,MAAM,UAA2B,IAAI,MAAM,SAAS,MAAM;CAC1D,IAAI,OAAO;CACX,MAAM,OAAO,YAA2B;EACtC,OAAO,OAAO,SAAS,QAAQ;GAC7B,MAAM,OAAO;GACb,QAAQ;GACR,MAAM,MAAM,SAAS;GACrB,IAAI,QAAQ,KAAA,GACV;GAEF,QAAQ,QAAQ,MAAM,QAAQ,OAAO,GAAG;EAC1C;CACF;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CAEhF,MAAM,YAAY,OAAO,YAAY,SAAS,KAAK,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;CAI1E,IAAI,QAAQ;CACZ,IAAI,UAAU;CACd,IAAI,SAAS;CACb,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,IACT,SAAS;MACJ,IAAI,OAAO,YAAY,KAAA,GAAW;EACvC,WAAW;EACX,UAAU,OAAO,YAAY;CAC/B,OACE,UAAU;CAGd,OAAO;EACL,OAAO,QAAQ;EACf;EACA;EACA;EACA,SAAS,IAAI,SAAS,SAAS;EAC/B;EACA;CACF;AACF;;;;;;;;;AAUA,SAAgB,oBAAoB,QAA+B;CACjE,MAAM,QAAkB,CAAC;CACzB,MAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,QAAQ,KAAK,IAAI,OAAO,IAAI,EAAE,GAAG,CAAC;CAChF,MAAM,KACJ,aAAa,OAAO,MAAM,GAAG,OAAO,MAAM,UAAU,OAAO,QAAQ,YAAY,OAAO,OAAO,SACxF,OAAO,UAAU,IAAI,KAAK,OAAO,QAAQ,sBAAsB,GAAG,aAAa,QAAQ,IAC9F;CACA,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,QAAQ,OAAO,UAAU;EAC/B,IAAI,QAAQ,GACV,MAAM,KAAK,KAAK,OAAO,IAAI,OAAO;CAEtC;CACA,KAAK,MAAM,UAAU,OAAO,SAAS;EACnC,IAAI,OAAO,IACT;EAEF,MAAM,MAAM,OAAO,WAAW,gBAAgB,OAAO,SAAS,aAAa;EAC3E,MAAM,KAAK,OAAO,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK;EACxF,IAAI,OAAO,WAAW,KAAA,GACpB,MAAM,KAAK,SAAS,OAAO,OAAO,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG,GAAG;CAE/E;CACA,OAAO,MAAM,KAAK,IAAI;AACxB"}
@@ -1,7 +1,7 @@
1
- import { A as normaliseFinding, B as coveredPathsOf, F as levelKey, G as isUnwritten, H as isMissingId, I as readRubricLevels, J as refusesEmpty, K as issue, L as countWords, N as LEVEL_PROBABILITY_TOLERANCE, O as keyPositionFinding, P as levelDistribution, R as DRAFT_ISSUE_SEVERITY, U as isRecord$6, V as covers, W as isUnset, X as GROUP_CAPTIONS_REVEAL_DICTATION, Z as groupCaptionsField, _t as containsWords, a as readSchema, bt as placeholderOrder, k as finding, pt as withValidationScope, q as pathKey, rt as alignDictation, t as getActivityTypeDescriptor, tt as UnknownActivityTypeError, vt as fold, xt as revealsAnswer, yt as numberedPassage, z as checkMedia } from "./registry-B23o9UWh.js";
2
- import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-CLMdk8i9.js";
3
- import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-BbvvH_Ob.js";
4
- //#region src/ai-material.ts
1
+ import { A as normaliseFinding, B as coveredPathsOf, F as levelKey, G as isUnwritten, H as isMissingId, I as readRubricLevels, J as refusesEmpty, K as issue, L as countWords, N as LEVEL_PROBABILITY_TOLERANCE, O as keyPositionFinding, P as levelDistribution, R as DRAFT_ISSUE_SEVERITY, U as isRecord$6, V as covers, W as isUnset, X as GROUP_CAPTIONS_REVEAL_DICTATION, Z as groupCaptionsField, _t as containsWords, a as readSchema, bt as placeholderOrder, k as finding, pt as withValidationScope, q as pathKey, rt as alignDictation, t as getActivityTypeDescriptor, tt as UnknownActivityTypeError, vt as fold, xt as revealsAnswer, yt as numberedPassage, z as checkMedia } from "./registry-D92hz9iA.js";
2
+ import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-DD1GHcLI.js";
3
+ import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-CeDk7mfM.js";
4
+ // src/ai-material.ts
5
5
  /**
6
6
  * What a model is told about the group a question sits in: the material the
7
7
  * learner reads and sees beside it.
@@ -89,8 +89,7 @@ function materialFacts(group) {
89
89
  ...locale !== void 0 ? { locale } : {}
90
90
  };
91
91
  }
92
- //#endregion
93
- //#region src/ai-text.ts
92
+ // src/ai-text.ts
94
93
  /**
95
94
  * How the SDK reads text a model wrote, before a learner does. Internal: shared
96
95
  * by the AI checks, and exported by none of the package's entry points.
@@ -178,8 +177,7 @@ function folded(text) {
178
177
  }
179
178
  /** A quote as it is searched for: folded, and without the spaces around it. */
180
179
  const needle = (quote) => folded(quote).chars.trim();
181
- //#endregion
182
- //#region src/ai.ts
180
+ // src/ai.ts
183
181
  /**
184
182
  * The activity types the SDK builds facts for, and which of them take hints.
185
183
  * A dictation takes none: its answer is the whole sentence, and it has
@@ -284,6 +282,11 @@ const base = (data) => ({
284
282
  title: data.title,
285
283
  ...typeof data.locale === "string" ? { locale: data.locale } : {}
286
284
  });
285
+ /** The item's instructions, trimmed; nothing for none, or for blank ones the learner never sees. */
286
+ function instructionsOf$1(data) {
287
+ const text = data.instructions;
288
+ return typeof text === "string" && text.trim() !== "" ? { instructions: text.trim() } : {};
289
+ }
287
290
  /**
288
291
  * The facts a model is given about one item and one answer. `details` are the
289
292
  * SDK's per-part verdicts — from its own scorer, or from a server's outcome in
@@ -303,6 +306,7 @@ function buildAiFacts(data, response, details) {
303
306
  return {
304
307
  ...base(data),
305
308
  activityType: "multiple-choice",
309
+ ...instructionsOf$1(data),
306
310
  question: item.question,
307
311
  mode: item.mode,
308
312
  options: item.options.map((option) => {
@@ -328,6 +332,7 @@ function buildAiFacts(data, response, details) {
328
332
  return {
329
333
  ...base(data),
330
334
  activityType: "fill-in-the-blanks",
335
+ ...instructionsOf$1(data),
331
336
  passage: numberedPassage(item.passage),
332
337
  blanks: placeholderOrder(item.passage).map((id, index) => {
333
338
  const blank = blanks.get(id);
@@ -353,6 +358,7 @@ function buildAiFacts(data, response, details) {
353
358
  return {
354
359
  ...base(data),
355
360
  activityType: "gap-select",
361
+ ...instructionsOf$1(data),
356
362
  passage: numberedPassage(item.passage),
357
363
  gaps: placeholderOrder(item.passage).map((id, index) => {
358
364
  const gap = gaps.get(id);
@@ -596,8 +602,7 @@ function hintRevealsAnswer(facts, hint) {
596
602
  default: return true;
597
603
  }
598
604
  }
599
- //#endregion
600
- //#region src/ai-coaching.ts
605
+ // src/ai-coaching.ts
601
606
  /** The most words one coaching may work on. More is refused, not cut. */
602
607
  const AI_COACHING_MAX_WORDS = 20;
603
608
  /** The longest tip for one word, in code points. */
@@ -865,8 +870,7 @@ function checkSound(raw, fact) {
865
870
  heard: instead.symbol
866
871
  };
867
872
  }
868
- //#endregion
869
- //#region src/media-refs.ts
873
+ // src/media-refs.ts
870
874
  const MAIN_KINDS = [
871
875
  "audio",
872
876
  "video",
@@ -1035,8 +1039,7 @@ function transcriptOf(holder) {
1035
1039
  if (!isRecord$3(holder)) return null;
1036
1040
  return ownText(holder) ?? (isRecord$3(holder.media) ? ownText(holder.media) : null);
1037
1041
  }
1038
- //#endregion
1039
- //#region src/authoring/recordings.ts
1042
+ // src/authoring/recordings.ts
1040
1043
  const written$1 = (value) => typeof value === "string" && value.trim() !== "";
1041
1044
  /** The script's turns that have a speaker and words, or none. */
1042
1045
  function turnsOf(script) {
@@ -1099,8 +1102,7 @@ function stimulusFindings(stimulus) {
1099
1102
  script: stimulus.script
1100
1103
  }, ["stimulus"]), ...holderFindings(stimulus.media, ["stimulus", "media"])];
1101
1104
  }
1102
- //#endregion
1103
- //#region src/authoring/item-group.ts
1105
+ // src/authoring/item-group.ts
1104
1106
  /**
1105
1107
  * A new, empty reading or listening testlet for an editor to start from.
1106
1108
  *
@@ -1494,8 +1496,7 @@ function checkItems(draft) {
1494
1496
  for (const id of repeated) issues.push(issue("ig_item_id_duplicate", ["items"], `More than one question has the id "${id}".`));
1495
1497
  return issues;
1496
1498
  }
1497
- //#endregion
1498
- //#region src/authoring/index.ts
1499
+ // src/authoring/index.ts
1499
1500
  /**
1500
1501
  * Tells a draft that is not finished from a draft that is wrong.
1501
1502
  *
@@ -1652,8 +1653,7 @@ function critiqueDraft(type, draft) {
1652
1653
  const critique = descriptor.authoring?.critique;
1653
1654
  return withValidationScope(() => [...critique === void 0 ? [] : critique(draft), ...recordingFindings(draft)]).map(normaliseFinding);
1654
1655
  }
1655
- //#endregion
1656
- //#region src/ai-critique.ts
1656
+ // src/ai-critique.ts
1657
1657
  /** The most findings one critique may carry. More is refused, not cut. */
1658
1658
  const AI_CRITIQUE_MAX_FINDINGS = 20;
1659
1659
  /** The longest message or quote in a finding, in code points. */
@@ -1701,10 +1701,10 @@ const NOT_WORDS = /* @__PURE__ */ new Set([
1701
1701
  "timeline"
1702
1702
  ]);
1703
1703
  /**
1704
- * Every string an author wrote into an item, with its path: the question, the
1705
- * options, the passage, the answers, the hints, the feedback, a picture's
1706
- * `alt`. Settings, ids and addresses are left out, and so is any `…Html`
1707
- * sidecar: the plain field beside it holds the same words.
1704
+ * Every string an author wrote into an item, with its path: the instructions,
1705
+ * the question, the options, the passage, the answers, the hints, the
1706
+ * feedback, a picture's `alt`. Settings, ids and addresses are left out, and
1707
+ * so is any `…Html` sidecar: the plain field beside it holds the same words.
1708
1708
  */
1709
1709
  function fieldsOf(value, path = []) {
1710
1710
  if (typeof value === "string") return value.trim() === "" ? [] : [{
@@ -1825,8 +1825,7 @@ function checkAiCritique(raw, request) {
1825
1825
  }
1826
1826
  };
1827
1827
  }
1828
- //#endregion
1829
- //#region src/ai-drafts.ts
1828
+ // src/ai-drafts.ts
1830
1829
  /** The most drafts one request may ask for, and the most a reply may hold when it names no count. */
1831
1830
  const AI_DRAFTS_MAX_COUNT = 50;
1832
1831
  /** The most repair passes `generateDrafts` makes. */
@@ -1918,7 +1917,7 @@ const FIELDS = {
1918
1917
  },
1919
1918
  transcript: text("What the learner hears and types: words from the source, as they are said there."),
1920
1919
  referenceText: text("What the learner reads aloud: a sentence or two from the source."),
1921
- instructions: text("Optional. How to read it, shown to the learner."),
1920
+ instructions: text("Optional. The task, shown to the learner before the item: what to do (\"Complete the sentence. Use ONE word.\"), or how to read it aloud. Never part of the text the learner answers."),
1922
1921
  prompt: text("The writing task, as plain text."),
1923
1922
  minWords: {
1924
1923
  type: "integer",
@@ -1950,6 +1949,7 @@ const FIELDS = {
1950
1949
  const WRITES = {
1951
1950
  "multiple-choice": [
1952
1951
  "title",
1952
+ "instructions",
1953
1953
  "question",
1954
1954
  "mode",
1955
1955
  "options",
@@ -1957,12 +1957,14 @@ const WRITES = {
1957
1957
  ],
1958
1958
  "fill-in-the-blanks": [
1959
1959
  "title",
1960
+ "instructions",
1960
1961
  "passage",
1961
1962
  "blanks",
1962
1963
  "feedback"
1963
1964
  ],
1964
1965
  "gap-select": [
1965
1966
  "title",
1967
+ "instructions",
1966
1968
  "passage",
1967
1969
  "gaps",
1968
1970
  "feedback"
@@ -2100,10 +2102,16 @@ function feedbackOf(entry) {
2100
2102
  ...incorrect !== void 0 ? { incorrect } : {}
2101
2103
  } };
2102
2104
  }
2105
+ /** The instructions a model wrote for the learner, when it wrote any. */
2106
+ function instructionsOf(entry) {
2107
+ const instructions = written(entry.instructions);
2108
+ return instructions !== void 0 ? { instructions } : {};
2109
+ }
2103
2110
  /** A type's content, read from what a model wrote. */
2104
2111
  function contentOf(type, entry, newId) {
2105
2112
  switch (type) {
2106
2113
  case "multiple-choice": return {
2114
+ ...instructionsOf(entry),
2107
2115
  question: stringOf(entry.question),
2108
2116
  ...typeof entry.mode === "string" ? { mode: entry.mode } : {},
2109
2117
  options: listOf(entry.options).map((option) => {
@@ -2119,6 +2127,7 @@ function contentOf(type, entry, newId) {
2119
2127
  ...feedbackOf(entry)
2120
2128
  };
2121
2129
  case "fill-in-the-blanks": return {
2130
+ ...instructionsOf(entry),
2122
2131
  passage: stringOf(entry.passage),
2123
2132
  blanks: listOf(entry.blanks).map((blank, index) => {
2124
2133
  const fields = isRecord$1(blank) ? blank : {};
@@ -2132,6 +2141,7 @@ function contentOf(type, entry, newId) {
2132
2141
  ...feedbackOf(entry)
2133
2142
  };
2134
2143
  case "gap-select": return {
2144
+ ...instructionsOf(entry),
2135
2145
  passage: stringOf(entry.passage),
2136
2146
  gaps: listOf(entry.gaps).map((gap, index) => {
2137
2147
  const marked = listOf(isRecord$1(gap) ? gap.choices : void 0);
@@ -2152,14 +2162,11 @@ function contentOf(type, entry, newId) {
2152
2162
  transcript: stringOf(entry.transcript),
2153
2163
  ...feedbackOf(entry)
2154
2164
  };
2155
- case "read-aloud": {
2156
- const instructions = written(entry.instructions);
2157
- return {
2158
- referenceText: stringOf(entry.referenceText),
2159
- ...instructions !== void 0 ? { instructions } : {},
2160
- ...feedbackOf(entry)
2161
- };
2162
- }
2165
+ case "read-aloud": return {
2166
+ referenceText: stringOf(entry.referenceText),
2167
+ ...instructionsOf(entry),
2168
+ ...feedbackOf(entry)
2169
+ };
2163
2170
  default: {
2164
2171
  const criteria = listOf(isRecord$1(entry.rubric) ? entry.rubric.criteria : void 0);
2165
2172
  return {
@@ -2530,8 +2537,7 @@ function interactiveVideoFromDrafts(input) {
2530
2537
  findings: critiqueItemGroupDraft(group)
2531
2538
  };
2532
2539
  }
2533
- //#endregion
2534
- //#region src/ai-written.ts
2540
+ // src/ai-written.ts
2535
2541
  /**
2536
2542
  * What the AI features about a written response share: reading a model's text
2537
2543
  * fields, and finding each correction's quote in the learner's text.
@@ -2674,8 +2680,7 @@ function checkCorrections(raw, text) {
2674
2680
  corrections
2675
2681
  };
2676
2682
  }
2677
- //#endregion
2678
- //#region src/ai-grading.ts
2683
+ // src/ai-grading.ts
2679
2684
  /**
2680
2685
  * How far a model's level probabilities may sum from 1 and still be a
2681
2686
  * distribution: rounding in a model's output, not a different judgement.
@@ -2999,8 +3004,7 @@ function checkAiGrading(raw, request) {
2999
3004
  }
3000
3005
  };
3001
3006
  }
3002
- //#endregion
3003
- //#region src/ai-writing.ts
3007
+ // src/ai-writing.ts
3004
3008
  /**
3005
3009
  * The request for feedback on a draft, or `null` when there is none to give:
3006
3010
  * not a written response, an author who switched explanations off (writing
@@ -3170,7 +3174,6 @@ function checkAiWritingFeedback(raw, request) {
3170
3174
  }
3171
3175
  };
3172
3176
  }
3173
- //#endregion
3174
3177
  export { AI_COACHING_MAX_TIP_LENGTH as A, aiHintRequest as B, createItemGroupDraft as C, listMediaRefs as D, validateItemGroupDraft as E, AI_HINT_TYPES as F, hintRevealsAnswer as G, buildAiFacts as H, AI_TEXT_MAX_LENGTH as I, readAiTextResult as K, aiAllowedByContent as L, aiCoachingRequest as M, checkAiCoaching as N, mapMediaRefs as O, AI_EXPLANATION_TYPES as P, aiExplanationRequest as R, createInteractiveVideoDraft as S, critiqueItemGroupDraft as T, checkAiExplanation as U, aiSupports as V, checkAiHint as W, aiCritiqueRequest as _, checkAiGrading as a, critiqueDraft as b, AI_DRAFTS_MAX_COUNT as c, aiDraftsRequest as d, checkAiDrafts as f, AI_CRITIQUE_MAX_FINDINGS as g, AI_CRITIQUE_MAX_FIELD_LENGTH as h, aiGradingRequest as i, AI_COACHING_MAX_WORDS as j, transcriptOf as k, AI_DRAFTS_MAX_REPAIRS as l, interactiveVideoFromDrafts as m, checkAiWritingFeedback as n, AI_WRITING_MAX_CORRECTIONS as o, generateDrafts as p, AI_MATERIAL_MAX_LENGTH as q, AI_GRADING_PROBABILITY_TOLERANCE as r, AI_WRITING_MAX_FIELD_LENGTH as s, aiWritingFeedbackRequest as t, aiDraftsRepairRequest as u, checkAiCritique as v, critiqueDrafts as w, validateDraft as x, createDraft as y, aiGradeOf as z };
3175
3178
 
3176
- //# sourceMappingURL=ai-writing-OJ5m_1EB.js.map
3179
+ //# sourceMappingURL=ai-writing-B7oMCt66.js.map