@intellectif/lk-core 1.3.0 → 1.4.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 (48) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/{ai-B222dtHH.d.ts → ai-CnmLPMzd.d.cts} +265 -2
  3. package/dist/ai-CnmLPMzd.d.cts.map +1 -0
  4. package/dist/{ai-B222dtHH.d.cts → ai-CnmLPMzd.d.ts} +265 -2
  5. package/dist/ai-CnmLPMzd.d.ts.map +1 -0
  6. package/dist/ai-check.cjs +101 -8
  7. package/dist/ai-check.cjs.map +1 -1
  8. package/dist/ai-check.d.cts +1 -1
  9. package/dist/ai-check.d.cts.map +1 -1
  10. package/dist/ai-check.d.ts +1 -1
  11. package/dist/ai-check.d.ts.map +1 -1
  12. package/dist/ai-check.js +101 -8
  13. package/dist/ai-check.js.map +1 -1
  14. package/dist/{ai-writing-CCBjTOM3.js → ai-writing-CPpH9Qxq.js} +126 -21
  15. package/dist/ai-writing-CPpH9Qxq.js.map +1 -0
  16. package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-G6tnCGzw.cjs} +114 -3
  17. package/dist/ai-writing-G6tnCGzw.cjs.map +1 -0
  18. package/dist/{index-BWPRPIXL.d.ts → index-2nigSf4N.d.cts} +3 -217
  19. package/dist/index-2nigSf4N.d.cts.map +1 -0
  20. package/dist/{index-O4_ExlAD.d.cts → index-B54baFUA.d.ts} +3 -217
  21. package/dist/index-B54baFUA.d.ts.map +1 -0
  22. package/dist/{index-Dyu66R4-.d.cts → index-BAfEHCnD.d.cts} +2 -2
  23. package/dist/{index-Dyu66R4-.d.cts.map → index-BAfEHCnD.d.cts.map} +1 -1
  24. package/dist/{index-CXyOV8sm.d.cts → index-BqMXWuNC.d.cts} +2 -2
  25. package/dist/{index-CXyOV8sm.d.cts.map → index-BqMXWuNC.d.cts.map} +1 -1
  26. package/dist/{index-UGxRSopE.d.ts → index-CVWvymVb.d.ts} +2 -2
  27. package/dist/{index-UGxRSopE.d.ts.map → index-CVWvymVb.d.ts.map} +1 -1
  28. package/dist/{index-CegM8o15.d.ts → index-CcHDTxIZ.d.ts} +2 -2
  29. package/dist/{index-CegM8o15.d.ts.map → index-CcHDTxIZ.d.ts.map} +1 -1
  30. package/dist/index.cjs +2 -1
  31. package/dist/index.d.cts +35 -7
  32. package/dist/index.d.cts.map +1 -1
  33. package/dist/index.d.ts +35 -7
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/schemas.d.cts +1 -1
  37. package/dist/schemas.d.ts +1 -1
  38. package/dist/scoring.d.cts +2 -2
  39. package/dist/scoring.d.ts +2 -2
  40. package/dist/xapi.d.cts +1 -1
  41. package/dist/xapi.d.ts +1 -1
  42. package/package.json +1 -1
  43. package/dist/ai-B222dtHH.d.cts.map +0 -1
  44. package/dist/ai-B222dtHH.d.ts.map +0 -1
  45. package/dist/ai-writing-CCBjTOM3.js.map +0 -1
  46. package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
  47. package/dist/index-BWPRPIXL.d.ts.map +0 -1
  48. package/dist/index-O4_ExlAD.d.cts.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 { 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 AiHintRequest,\n AiWritingFeedbackRequest,\n} from '../types/ai.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 */\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\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\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 /** 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}\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): AiCheckCase {\n const request = aiExplanationRequest({ data, response });\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): AiCheckCase {\n const request = aiHintRequest({ data, response, previousHints });\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): AiCheckCase {\n const request = aiWritingFeedbackRequest({\n data: weekend,\n response: drafted(text),\n previousFeedback,\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 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; and drafts of an essay, with\n * mistakes, without, and revised.\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 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 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 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 ];\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 * });\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.\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 { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\n AiCritique,\n AiCritiqueRequest,\n AiDrafts,\n AiDraftsRequest,\n AiExplanationRequest,\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}\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 /** 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\nconst REFUSALS: AiRefusal[] = [\n 'malformed',\n 'empty',\n 'too-long',\n 'contradicts-grade',\n 'reveals-answer',\n 'misquotes-answer',\n 'contradicts-marks',\n 'contradicts-item',\n];\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 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 === '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 { 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 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 */\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\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 /** 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}\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 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; and drafts of an essay, with\n * mistakes, without, and revised.\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 ];\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 * });\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.\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 { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\n AiCritique,\n AiCritiqueRequest,\n AiDrafts,\n AiDraftsRequest,\n AiExplanationRequest,\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}\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 /** 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\nconst REFUSALS: AiRefusal[] = [\n 'malformed',\n 'empty',\n 'too-long',\n 'contradicts-grade',\n 'reveals-answer',\n 'misquotes-answer',\n 'contradicts-marks',\n 'contradicts-item',\n];\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 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 === '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,6 +1,95 @@
1
- import { B as issue, D as keyPositionFinding, F as covers, G as groupCaptionsField, H as refusesEmpty, I as isMissingId, L as isRecord$4, M as DRAFT_ISSUE_SEVERITY, N as checkMedia, O as normaliseFinding, P as coveredPathsOf, R as isUnset, V as pathKey, W as GROUP_CAPTIONS_REVEAL_DICTATION, Y as UnknownActivityTypeError, Z as alignDictation, a as readSchema, dt as containsWords, ft as fold, ht as revealsAnswer, j as countWords, mt as placeholderOrder, pt as numberedPassage, st as withValidationScope, t as getActivityTypeDescriptor, z as isUnwritten } from "./registry-Dsx9mNsS.js";
1
+ import { B as issue, D as keyPositionFinding, F as covers, G as groupCaptionsField, H as refusesEmpty, I as isMissingId, L as isRecord$5, M as DRAFT_ISSUE_SEVERITY, N as checkMedia, O as normaliseFinding, P as coveredPathsOf, R as isUnset, V as pathKey, W as GROUP_CAPTIONS_REVEAL_DICTATION, Y as UnknownActivityTypeError, Z as alignDictation, a as readSchema, dt as containsWords, ft as fold, ht as revealsAnswer, j as countWords, mt as placeholderOrder, pt as numberedPassage, st as withValidationScope, t as getActivityTypeDescriptor, z as isUnwritten } from "./registry-Dsx9mNsS.js";
2
2
  import { c as validateSpeechAssessment, d as gradeFromRubric, r as score, s as alignReadAloud, w as isGradeInRange } from "./scoring-BZqUkuDF.js";
3
3
  import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-DXiR8nOr.js";
4
+ //#region src/ai-material.ts
5
+ /**
6
+ * What a model is told about the group a question sits in: the material the
7
+ * learner reads and sees beside it.
8
+ *
9
+ * Internal: the request builders in `ai.ts` and `ai-writing.ts` are its public
10
+ * face, through their `group` input.
11
+ */
12
+ /**
13
+ * The longest material text a model is given, in code points. A longer one is
14
+ * cut and marked `truncated`: a long reading passage on an exam paper runs to
15
+ * 4,000–7,000.
16
+ */
17
+ const AI_MATERIAL_MAX_LENGTH = 12e3;
18
+ const KINDS$2 = {
19
+ text: true,
20
+ audio: true,
21
+ video: true,
22
+ image: true,
23
+ mixed: true
24
+ };
25
+ const MEDIA_TYPES = {
26
+ image: true,
27
+ audio: true,
28
+ video: true,
29
+ embed: true
30
+ };
31
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
32
+ /** A string with something in it, as written; otherwise nothing. */
33
+ const filled = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
34
+ /**
35
+ * `text` cut to {@link AI_MATERIAL_MAX_LENGTH} code points. The cut falls at
36
+ * the last space in its second half, so no word is broken; in a script written
37
+ * without spaces, at the limit itself.
38
+ */
39
+ function cut(text) {
40
+ const points = [...text];
41
+ if (points.length <= 12e3) return {
42
+ text,
43
+ truncated: false
44
+ };
45
+ let end = AI_MATERIAL_MAX_LENGTH;
46
+ for (let at = AI_MATERIAL_MAX_LENGTH; at >= AI_MATERIAL_MAX_LENGTH / 2; at -= 1) if (/\s/u.test(points[at])) {
47
+ end = at;
48
+ break;
49
+ }
50
+ return {
51
+ text: points.slice(0, end).join("").trimEnd(),
52
+ truncated: true
53
+ };
54
+ }
55
+ /**
56
+ * The facts of `group`'s material, or `null` without a group or a stimulus it
57
+ * can read: not an object, or of a kind the SDK does not know.
58
+ *
59
+ * Only what the learner sees reaches a model. The `transcript` is author-only
60
+ * and never read, so the facts are the same for a whole group and a redacted
61
+ * one; a media address is never read either, since a model cannot open one
62
+ * and a signed address must not end up in a prompt. Each field is read once.
63
+ */
64
+ function materialFacts(group) {
65
+ if (!isRecord$4(group)) return null;
66
+ const stimulus = group.stimulus;
67
+ if (!isRecord$4(stimulus)) return null;
68
+ const kind = stimulus.kind;
69
+ if (typeof kind !== "string" || !Object.hasOwn(KINDS$2, kind)) return null;
70
+ const title = filled(stimulus.title) ?? filled(group.title);
71
+ const body = filled(stimulus.body);
72
+ const text = body === void 0 ? void 0 : cut(body);
73
+ const rawMedia = stimulus.media;
74
+ const mediaType = isRecord$4(rawMedia) ? rawMedia.type : void 0;
75
+ const alt = isRecord$4(rawMedia) ? filled(rawMedia.alt) : void 0;
76
+ const media = typeof mediaType === "string" && Object.hasOwn(MEDIA_TYPES, mediaType) ? {
77
+ type: mediaType,
78
+ ...alt !== void 0 ? { alt } : {}
79
+ } : void 0;
80
+ const attribution = filled(stimulus.attribution);
81
+ const locale = filled(stimulus.locale);
82
+ return {
83
+ kind,
84
+ ...title !== void 0 ? { title } : {},
85
+ ...text !== void 0 ? { text: text.text } : {},
86
+ ...text?.truncated === true ? { truncated: true } : {},
87
+ ...media !== void 0 ? { media } : {},
88
+ ...attribution !== void 0 ? { attribution } : {},
89
+ ...locale !== void 0 ? { locale } : {}
90
+ };
91
+ }
92
+ //#endregion
4
93
  //#region src/ai-text.ts
5
94
  /**
6
95
  * How the SDK reads text a model wrote, before a learner does. Internal: shared
@@ -341,6 +430,10 @@ function gradeLocally(data, response) {
341
430
  * positive and finite, `score` finite from 0 to it) gets no explanation at all:
342
431
  * explaining 85 "out of 1" would tell the model a wrong answer was correct, and
343
432
  * grading locally instead would speak to a grade the screen does not show.
433
+ *
434
+ * `group` is the item group the question sits in: its material — the passage,
435
+ * or the instructions the group's questions share — goes with the request as
436
+ * `material`, as the learner sees it.
344
437
  */
345
438
  function aiExplanationRequest(input) {
346
439
  const { data, response, outcome, learnerLocale } = input;
@@ -366,10 +459,12 @@ function aiExplanationRequest(input) {
366
459
  if (grade === null) return null;
367
460
  const facts = buildAiFacts(data, response, details);
368
461
  if (facts === null) return null;
462
+ const material = materialFacts(input.group);
369
463
  return {
370
464
  feature: "explanation",
371
465
  facts,
372
466
  grade,
467
+ ...material !== null ? { material } : {},
373
468
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
374
469
  };
375
470
  }
@@ -380,7 +475,11 @@ function aiExplanationRequest(input) {
380
475
  *
381
476
  * The facts carry the key, and which parts the learner has right so far, so a
382
477
  * model can hint where it helps. The learner never sees them; a hint that
383
- * contains an answer is refused by {@link checkAiHint}.
478
+ * contains an answer is refused by {@link checkAiHint}, whatever it quotes —
479
+ * the group's material included.
480
+ *
481
+ * `group` is the item group the question sits in, as for
482
+ * {@link aiExplanationRequest}.
384
483
  */
385
484
  function aiHintRequest(input) {
386
485
  const { data, previousHints, learnerLocale } = input;
@@ -388,11 +487,13 @@ function aiHintRequest(input) {
388
487
  const response = input.response ?? emptyResponse(data.type);
389
488
  const facts = buildAiFacts(data, response, (response === null ? null : gradeLocally(data, response))?.details ?? null);
390
489
  if (facts === null) return null;
490
+ const material = materialFacts(input.group);
391
491
  return {
392
492
  feature: "hint",
393
493
  facts,
394
494
  hintNumber: previousHints.length + 1,
395
495
  previousHints: [...previousHints],
496
+ ...material !== null ? { material } : {},
396
497
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
397
498
  };
398
499
  }
@@ -860,7 +961,7 @@ function createInteractiveVideoDraft(context) {
860
961
  * {@link validateItemGroupDraft} already reports it.
861
962
  */
862
963
  function critiqueItemGroupDraft(draft) {
863
- if (!isRecord$4(draft)) return [];
964
+ if (!isRecord$5(draft)) return [];
864
965
  const items = Array.isArray(draft.items) ? draft.items : [];
865
966
  return withValidationScope(() => [...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])), ...keyPositionFinding(items, ["items"])]);
866
967
  }
@@ -872,11 +973,11 @@ function critiqueItemGroupDraft(draft) {
872
973
  * skipped.
873
974
  */
874
975
  function critiqueDrafts(entries) {
875
- return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$4(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
976
+ return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$5(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
876
977
  }
877
978
  /** One item's findings, or none for an item the critic cannot place. */
878
979
  function critiqueItem(item) {
879
- if (!isRecord$4(item) || typeof item.type !== "string") return [];
980
+ if (!isRecord$5(item) || typeof item.type !== "string") return [];
880
981
  if (getActivityTypeDescriptor(item.type) === void 0) return [];
881
982
  return critiqueDraft(item.type, item);
882
983
  }
@@ -889,7 +990,7 @@ function validateItemGroupDraft(draft) {
889
990
  }
890
991
  function validateItemGroupDraftInScope(draft) {
891
992
  const issues = [];
892
- if (!isRecord$4(draft)) {
993
+ if (!isRecord$5(draft)) {
893
994
  issues.push(issue("ig_not_an_object", [], "A group must be an object with a stimulus and its items."));
894
995
  return {
895
996
  status: "invalid",
@@ -897,7 +998,7 @@ function validateItemGroupDraftInScope(draft) {
897
998
  };
898
999
  }
899
1000
  issues.push(...checkGroupIdentity(draft));
900
- if (isRecord$4(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
1001
+ if (isRecord$5(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
901
1002
  else if (isUnset(draft.stimulus)) issues.push(issue("ig_stimulus_required", ["stimulus"], "Add the passage, recording or image the questions are about."));
902
1003
  issues.push(...checkItems(draft));
903
1004
  if (draft.timeline !== void 0 && draft.timeline !== null) issues.push(...checkTimeline(draft));
@@ -947,10 +1048,10 @@ function validateItemGroupDraftInScope(draft) {
947
1048
  function checkTimeline(draft) {
948
1049
  const issues = [];
949
1050
  const timeline = draft.timeline;
950
- if (!isRecord$4(timeline)) return issues;
951
- const stimulus = isRecord$4(draft.stimulus) ? draft.stimulus : void 0;
1051
+ if (!isRecord$5(timeline)) return issues;
1052
+ const stimulus = isRecord$5(draft.stimulus) ? draft.stimulus : void 0;
952
1053
  if (stimulus !== void 0 && !isUnwritten(stimulus.kind) && stimulus.kind !== "video") issues.push(issue("ig_timeline_stimulus_kind", ["stimulus", "kind"], "An interactive video needs a video: quizzes open at moments of it."));
953
- else if (stimulus !== void 0 && isRecord$4(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
1054
+ else if (stimulus !== void 0 && isRecord$5(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
954
1055
  "stimulus",
955
1056
  "media",
956
1057
  "type"
@@ -959,7 +1060,7 @@ function checkTimeline(draft) {
959
1060
  const items = Array.isArray(draft.items) ? draft.items : [];
960
1061
  const itemIndexById = /* @__PURE__ */ new Map();
961
1062
  items.forEach((item, index) => {
962
- if (!isRecord$4(item)) return;
1063
+ if (!isRecord$5(item)) return;
963
1064
  if (typeof item.id === "string" && item.id !== "") itemIndexById.set(item.id, index);
964
1065
  const type = item.type;
965
1066
  if (typeof type === "string" && type !== "" && type !== "item-group" && !isInteractiveVideoItemType(type) && getActivityTypeDescriptor(type) !== void 0) issues.push(issue("ig_timeline_item_type", [
@@ -977,7 +1078,7 @@ function checkTimeline(draft) {
977
1078
  const quizIds = /* @__PURE__ */ new Set();
978
1079
  const placed = /* @__PURE__ */ new Set();
979
1080
  cues.forEach((cue, cueIndex) => {
980
- if (!isRecord$4(cue)) return;
1081
+ if (!isRecord$5(cue)) return;
981
1082
  const at = (field, ...rest) => [
982
1083
  "timeline",
983
1084
  "cues",
@@ -1008,7 +1109,7 @@ function checkTimeline(draft) {
1008
1109
  const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
1009
1110
  let previousAt;
1010
1111
  chapters.forEach((chapter, index) => {
1011
- if (!isRecord$4(chapter)) return;
1112
+ if (!isRecord$5(chapter)) return;
1012
1113
  if (isUnwritten(chapter.title)) issues.push(issue("ig_timeline_chapter_title", [
1013
1114
  "timeline",
1014
1115
  "chapters",
@@ -1069,14 +1170,14 @@ function checkStimulus(stimulus) {
1069
1170
  if (isUnwritten(kind)) issues.push(issue("ig_stimulus_kind_required", at("kind"), "Choose what the questions are about: a text, a recording, a video, an image, or a mix."));
1070
1171
  else if (typeof kind !== "string" || !KINDS$1.has(kind)) {
1071
1172
  if (typeof kind === "string") issues.push(issue("ig_stimulus_kind_invalid", at("kind"), `"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`));
1072
- if (isRecord$4(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
1173
+ if (isRecord$5(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
1073
1174
  return issues;
1074
1175
  }
1075
1176
  const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
1076
1177
  const htmlPresent = !isUnset(stimulus.bodyHtml);
1077
1178
  if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) issues.push(issue("ig_stimulus_body_required", at("body"), isUnwritten(stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."));
1078
1179
  if (kind !== "text" && isUnset(stimulus.media)) issues.push(issue("ig_stimulus_media_required", at("media"), "Add the recording, video or image."));
1079
- else if (isRecord$4(stimulus.media)) {
1180
+ else if (isRecord$5(stimulus.media)) {
1080
1181
  const allowed = MEDIA_FOR_KIND[kind];
1081
1182
  const mediaType = stimulus.media.type;
1082
1183
  if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) issues.push(issue("ig_stimulus_media_kind", at("media", "type"), `A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`));
@@ -1103,7 +1204,7 @@ function checkItems(draft) {
1103
1204
  const seen = /* @__PURE__ */ new Set();
1104
1205
  const repeated = /* @__PURE__ */ new Set();
1105
1206
  for (const [index, item] of items.entries()) {
1106
- if (!isRecord$4(item)) continue;
1207
+ if (!isRecord$5(item)) continue;
1107
1208
  const ordinal = index + 1;
1108
1209
  if (isMissingId(item.id)) issues.push(issue("ig_item_id_required", [
1109
1210
  "items",
@@ -1200,7 +1301,7 @@ function validateDraft(type, draft) {
1200
1301
  function validateDraftInScope(type, draft) {
1201
1302
  const descriptor = getActivityTypeDescriptor(type);
1202
1303
  if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
1203
- const reported = isRecord$4(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
1304
+ const reported = isRecord$5(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
1204
1305
  const issues = [...reported];
1205
1306
  const parsed = readSchema(descriptor.schema, draft, `Activity type "${descriptor.type}"`, { reportInput: true });
1206
1307
  if (!parsed.success) {
@@ -1310,7 +1411,7 @@ function critiqueDraft(type, draft) {
1310
1411
  const descriptor = getActivityTypeDescriptor(type);
1311
1412
  if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
1312
1413
  const critique = descriptor.authoring?.critique;
1313
- if (!isRecord$4(draft) || critique === void 0) return [];
1414
+ if (!isRecord$5(draft) || critique === void 0) return [];
1314
1415
  return withValidationScope(() => critique(draft)).map(normaliseFinding);
1315
1416
  }
1316
1417
  //#endregion
@@ -2204,7 +2305,9 @@ const MAX_TAG_LENGTH = 40;
2204
2305
  * The facts are the task as the learner saw it, the draft verbatim with its
2205
2306
  * word count recomputed as the grader will count it, and the rubric the answer
2206
2307
  * will be graded on. `previousFeedback` is the text of the feedback already
2207
- * shown on earlier drafts, so a model can speak to what changed.
2308
+ * shown on earlier drafts, so a model can speak to what changed. `group` is the
2309
+ * item group the task sits in: its material, the text a summary is of, goes
2310
+ * with the request as `material`.
2208
2311
  */
2209
2312
  function aiWritingFeedbackRequest(input) {
2210
2313
  const { data, response, learnerLocale } = input;
@@ -2222,6 +2325,7 @@ function aiWritingFeedbackRequest(input) {
2222
2325
  weight: criterion.weight
2223
2326
  }));
2224
2327
  const previousFeedback = [...input.previousFeedback ?? []];
2328
+ const material = materialFacts(input.group);
2225
2329
  return {
2226
2330
  feature: "writing-feedback",
2227
2331
  facts: {
@@ -2240,6 +2344,7 @@ function aiWritingFeedbackRequest(input) {
2240
2344
  },
2241
2345
  draftNumber: previousFeedback.length + 1,
2242
2346
  previousFeedback,
2347
+ ...material !== null ? { material } : {},
2243
2348
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
2244
2349
  };
2245
2350
  }
@@ -2491,6 +2596,6 @@ function checkAiWritingFeedback(raw, request) {
2491
2596
  };
2492
2597
  }
2493
2598
  //#endregion
2494
- export { AI_TEXT_MAX_LENGTH as A, readAiTextResult as B, validateItemGroupDraft as C, checkAiCoaching as D, aiCoachingRequest as E, aiSupports as F, buildAiFacts as I, checkAiExplanation as L, aiExplanationRequest as M, aiGradeOf as N, AI_EXPLANATION_TYPES as O, aiHintRequest as P, checkAiHint as R, critiqueItemGroupDraft as S, AI_COACHING_MAX_WORDS as T, critiqueDraft as _, AI_DRAFTS_MAX_COUNT as a, createItemGroupDraft as b, aiDraftsRequest as c, interactiveVideoFromDrafts as d, AI_CRITIQUE_MAX_FIELD_LENGTH as f, createDraft as g, checkAiCritique as h, checkAiWritingFeedback as i, aiAllowedByContent as j, AI_HINT_TYPES as k, checkAiDrafts as l, aiCritiqueRequest as m, AI_WRITING_MAX_FIELD_LENGTH as n, AI_DRAFTS_MAX_REPAIRS as o, AI_CRITIQUE_MAX_FINDINGS as p, aiWritingFeedbackRequest as r, aiDraftsRepairRequest as s, AI_WRITING_MAX_CORRECTIONS as t, generateDrafts as u, validateDraft as v, AI_COACHING_MAX_TIP_LENGTH as w, critiqueDrafts as x, createInteractiveVideoDraft as y, hintRevealsAnswer as z };
2599
+ export { AI_TEXT_MAX_LENGTH as A, readAiTextResult as B, validateItemGroupDraft as C, checkAiCoaching as D, aiCoachingRequest as E, aiSupports as F, buildAiFacts as I, checkAiExplanation as L, aiExplanationRequest as M, aiGradeOf as N, AI_EXPLANATION_TYPES as O, aiHintRequest as P, checkAiHint as R, critiqueItemGroupDraft as S, AI_COACHING_MAX_WORDS as T, AI_MATERIAL_MAX_LENGTH as V, critiqueDraft as _, AI_DRAFTS_MAX_COUNT as a, createItemGroupDraft as b, aiDraftsRequest as c, interactiveVideoFromDrafts as d, AI_CRITIQUE_MAX_FIELD_LENGTH as f, createDraft as g, checkAiCritique as h, checkAiWritingFeedback as i, aiAllowedByContent as j, AI_HINT_TYPES as k, checkAiDrafts as l, aiCritiqueRequest as m, AI_WRITING_MAX_FIELD_LENGTH as n, AI_DRAFTS_MAX_REPAIRS as o, AI_CRITIQUE_MAX_FINDINGS as p, aiWritingFeedbackRequest as r, aiDraftsRepairRequest as s, AI_WRITING_MAX_CORRECTIONS as t, generateDrafts as u, validateDraft as v, AI_COACHING_MAX_TIP_LENGTH as w, critiqueDrafts as x, createInteractiveVideoDraft as y, hintRevealsAnswer as z };
2495
2600
 
2496
- //# sourceMappingURL=ai-writing-CCBjTOM3.js.map
2601
+ //# sourceMappingURL=ai-writing-CPpH9Qxq.js.map