@intellectif/lk-core 0.22.0 → 1.1.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 (66) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +6 -6
  3. package/dist/{activity-CYc7SD3d.d.cts → activity-MBOYLBjR.d.cts} +128 -22
  4. package/dist/{activity-CYc7SD3d.d.ts → activity-MBOYLBjR.d.ts} +128 -22
  5. package/dist/ai-check.cjs +138 -9
  6. package/dist/ai-check.cjs.map +1 -1
  7. package/dist/ai-check.d.cts +6 -3
  8. package/dist/ai-check.d.ts +6 -3
  9. package/dist/ai-check.js +133 -4
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{chunk-2CV6NU32.cjs → chunk-2JT2WUQ2.cjs} +77 -39
  12. package/dist/chunk-2JT2WUQ2.cjs.map +1 -0
  13. package/dist/{chunk-D4TOPSHV.cjs → chunk-DSDXL3JO.cjs} +1216 -1105
  14. package/dist/chunk-DSDXL3JO.cjs.map +1 -0
  15. package/dist/{chunk-D3GP5MET.js → chunk-FBKTION6.js} +1093 -982
  16. package/dist/chunk-FBKTION6.js.map +1 -0
  17. package/dist/{chunk-Q7MV3IXJ.js → chunk-IARLJ5MW.js} +15 -16
  18. package/dist/chunk-IARLJ5MW.js.map +1 -0
  19. package/dist/{chunk-OPMY2MNS.cjs → chunk-LDFQUZII.cjs} +248 -22
  20. package/dist/chunk-LDFQUZII.cjs.map +1 -0
  21. package/dist/{chunk-5UCR4UNR.js → chunk-NQHETFDA.js} +238 -12
  22. package/dist/chunk-NQHETFDA.js.map +1 -0
  23. package/dist/{chunk-X5DAXBJI.js → chunk-SJVRUPJT.js} +2 -2
  24. package/dist/{chunk-EOFWSGLU.cjs → chunk-TJJFKKH2.cjs} +4 -4
  25. package/dist/{chunk-EOFWSGLU.cjs.map → chunk-TJJFKKH2.cjs.map} +1 -1
  26. package/dist/{chunk-WQVZPIN7.cjs → chunk-VSH7ECQL.cjs} +44 -45
  27. package/dist/chunk-VSH7ECQL.cjs.map +1 -0
  28. package/dist/{chunk-Z7QQZ53L.js → chunk-XS6U2ARZ.js} +70 -32
  29. package/dist/chunk-XS6U2ARZ.js.map +1 -0
  30. package/dist/index-DlCW0XaQ.d.cts +548 -0
  31. package/dist/index-DlcqkM_L.d.ts +548 -0
  32. package/dist/index.cjs +129 -198
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +178 -21
  35. package/dist/index.d.ts +178 -21
  36. package/dist/index.js +34 -103
  37. package/dist/index.js.map +1 -1
  38. package/dist/schemas.cjs +3 -94
  39. package/dist/schemas.cjs.map +1 -1
  40. package/dist/schemas.d.cts +2 -3
  41. package/dist/schemas.d.ts +2 -3
  42. package/dist/schemas.js +6 -97
  43. package/dist/scoring.cjs +3 -3
  44. package/dist/scoring.d.cts +242 -242
  45. package/dist/scoring.d.ts +242 -242
  46. package/dist/scoring.js +2 -2
  47. package/dist/xapi.cjs +3 -3
  48. package/dist/xapi.d.cts +1 -1
  49. package/dist/xapi.d.ts +1 -1
  50. package/dist/xapi.js +2 -2
  51. package/package.json +4 -4
  52. package/vectors/README.md +32 -2
  53. package/vectors/scoring.json +1 -498
  54. package/vectors/validation.json +6742 -0
  55. package/vectors/validation.mjs +195 -0
  56. package/dist/chunk-2CV6NU32.cjs.map +0 -1
  57. package/dist/chunk-5UCR4UNR.js.map +0 -1
  58. package/dist/chunk-D3GP5MET.js.map +0 -1
  59. package/dist/chunk-D4TOPSHV.cjs.map +0 -1
  60. package/dist/chunk-OPMY2MNS.cjs.map +0 -1
  61. package/dist/chunk-Q7MV3IXJ.js.map +0 -1
  62. package/dist/chunk-WQVZPIN7.cjs.map +0 -1
  63. package/dist/chunk-Z7QQZ53L.js.map +0 -1
  64. package/dist/index-0Z3-nnAP.d.ts +0 -2303
  65. package/dist/index-CxxiNSSH.d.cts +0 -2303
  66. /package/dist/{chunk-X5DAXBJI.js.map → chunk-SJVRUPJT.js.map} +0 -0
package/dist/ai-check.cjs CHANGED
@@ -5,9 +5,13 @@
5
5
 
6
6
 
7
7
 
8
- var _chunkOPMY2MNScjs = require('./chunk-OPMY2MNS.cjs');
9
- require('./chunk-WQVZPIN7.cjs');
10
- require('./chunk-D4TOPSHV.cjs');
8
+
9
+
10
+ var _chunkLDFQUZIIcjs = require('./chunk-LDFQUZII.cjs');
11
+
12
+
13
+ var _chunkVSH7ECQLcjs = require('./chunk-VSH7ECQL.cjs');
14
+ require('./chunk-DSDXL3JO.cjs');
11
15
 
12
16
  // src/ai-check/cases.ts
13
17
  var capital = {
@@ -87,6 +91,68 @@ var weekend = {
87
91
  ]
88
92
  }
89
93
  };
94
+ var weather = {
95
+ schemaVersion: "1.0",
96
+ type: "read-aloud",
97
+ id: "ai-check-reading",
98
+ title: "Read the forecast aloud",
99
+ referenceText: "The weather is warm in the north.",
100
+ locale: "en-US",
101
+ recording: { maxSeconds: 20 },
102
+ scoring: {
103
+ dimensions: [
104
+ { name: "accuracy", weight: 2 },
105
+ { name: "fluency", weight: 1 }
106
+ ]
107
+ }
108
+ };
109
+ var said = (text, over = {}) => ({
110
+ text,
111
+ error: "none",
112
+ accuracy: 92,
113
+ ...over
114
+ });
115
+ function forecast(slips) {
116
+ return {
117
+ assessmentVersion: "1.0",
118
+ status: "assessed",
119
+ task: "scripted",
120
+ locale: "en-US",
121
+ referenceText: weather.referenceText,
122
+ recordingKey: "ai-check-take",
123
+ assessor: { kind: "auto", id: "pronunciation-engine" },
124
+ scale: 100,
125
+ scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },
126
+ miscue: "assessor",
127
+ phonemeAlphabet: "ipa",
128
+ words: [
129
+ said("The"),
130
+ slips ? said("weather", {
131
+ accuracy: 38,
132
+ error: "mispronunciation",
133
+ phonemes: [
134
+ { symbol: "w", accuracy: 90 },
135
+ { symbol: "\u025B", accuracy: 85 },
136
+ {
137
+ symbol: "\xF0",
138
+ accuracy: 12,
139
+ heardAs: [
140
+ { symbol: "d", score: 64 },
141
+ { symbol: "z", score: 21 },
142
+ { symbol: "\xF0", score: 12 }
143
+ ]
144
+ },
145
+ { symbol: "\u025A", accuracy: 80 }
146
+ ]
147
+ }) : said("weather"),
148
+ said("is"),
149
+ said("warm"),
150
+ said("in"),
151
+ said("the"),
152
+ slips ? { text: "north", error: "omission" } : said("north")
153
+ ]
154
+ };
155
+ }
90
156
  var chose = (ids) => ({
91
157
  type: "multiple-choice",
92
158
  selectedOptionIds: ids
@@ -106,21 +172,21 @@ var drafted = (text) => ({
106
172
  wordCount: 0
107
173
  });
108
174
  function explanationCase(id, about, data, response) {
109
- const request = _chunkOPMY2MNScjs.aiExplanationRequest.call(void 0, { data, response });
175
+ const request = _chunkLDFQUZIIcjs.aiExplanationRequest.call(void 0, { data, response });
110
176
  if (request === null) {
111
177
  throw new Error(`ai-check: no explanation request for "${id}"`);
112
178
  }
113
179
  return { id, feature: "explanation", about, request };
114
180
  }
115
181
  function hintCase(id, about, data, response, previousHints = []) {
116
- const request = _chunkOPMY2MNScjs.aiHintRequest.call(void 0, { data, response, previousHints });
182
+ const request = _chunkLDFQUZIIcjs.aiHintRequest.call(void 0, { data, response, previousHints });
117
183
  if (request === null) {
118
184
  throw new Error(`ai-check: no hint request for "${id}"`);
119
185
  }
120
186
  return { id, feature: "hint", about, request };
121
187
  }
122
188
  function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
123
- const request = _chunkOPMY2MNScjs.aiWritingFeedbackRequest.call(void 0, {
189
+ const request = _chunkLDFQUZIIcjs.aiWritingFeedbackRequest.call(void 0, {
124
190
  data: weekend,
125
191
  response: drafted(text),
126
192
  previousFeedback,
@@ -131,6 +197,31 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
131
197
  }
132
198
  return { id, feature: "writing-feedback", about, request };
133
199
  }
200
+ function coachingCase(id, about, marks, learnerLocale) {
201
+ const assessment = _nullishCoalesce(marks.assessment, () => ( forecast(true)));
202
+ const grade = _chunkVSH7ECQLcjs.gradeReadAloud.call(void 0,
203
+ weather,
204
+ { type: "read-aloud", recording: { key: "ai-check-take", mimeType: "audio/wav" } },
205
+ assessment,
206
+ {
207
+ measured: { durationMs: 4e3, voicedMs: 3e3 },
208
+ plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 }
209
+ }
210
+ );
211
+ if ("unscorable" in grade) {
212
+ throw new Error(`ai-check: the reading for "${id}" did not grade: ${grade.reason}`);
213
+ }
214
+ const request = _chunkLDFQUZIIcjs.aiCoachingRequest.call(void 0, {
215
+ data: weather,
216
+ ...marks.fromGradeOnly === true ? {} : { assessment },
217
+ grade,
218
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
219
+ });
220
+ if (request === null) {
221
+ throw new Error(`ai-check: no coaching request for "${id}"`);
222
+ }
223
+ return { id, feature: "pronunciation-coaching", about, request };
224
+ }
134
225
  function aiCheckCases() {
135
226
  return [
136
227
  explanationCase("mc-correct", "Chose the right city", capital, chose(["madrid"])),
@@ -220,6 +311,23 @@ function aiCheckCases() {
220
311
  "Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.",
221
312
  [],
222
313
  "es"
314
+ ),
315
+ coachingCase("coaching-slips", 'A reading with a mispronounced "th" and a word left out', {
316
+ assessment: forecast(true)
317
+ }),
318
+ coachingCase("coaching-clean", "A reading the engine marked correct throughout", {
319
+ assessment: forecast(false)
320
+ }),
321
+ coachingCase(
322
+ "coaching-from-grade",
323
+ "The same slips, from a stored grade: marks without sounds",
324
+ { fromGradeOnly: true }
325
+ ),
326
+ coachingCase(
327
+ "coaching-in-spanish",
328
+ "The same slips, with coaching asked for in Spanish",
329
+ { assessment: forecast(true) },
330
+ "es"
223
331
  )
224
332
  ];
225
333
  }
@@ -231,7 +339,8 @@ var REFUSALS = [
231
339
  "too-long",
232
340
  "contradicts-grade",
233
341
  "reveals-answer",
234
- "misquotes-answer"
342
+ "misquotes-answer",
343
+ "contradicts-marks"
235
344
  ];
236
345
  function portFor(ports, one) {
237
346
  switch (one.request.feature) {
@@ -239,6 +348,8 @@ function portFor(ports, one) {
239
348
  return ports.explain;
240
349
  case "hint":
241
350
  return ports.hint;
351
+ case "pronunciation-coaching":
352
+ return ports.pronunciationCoaching;
242
353
  default:
243
354
  return ports.writingFeedback;
244
355
  }
@@ -262,8 +373,26 @@ async function runCase(ports, one) {
262
373
  };
263
374
  }
264
375
  const ms = now() - started;
376
+ if (one.request.feature === "pronunciation-coaching") {
377
+ const checked2 = _chunkLDFQUZIIcjs.checkAiCoaching.call(void 0, raw, one.request);
378
+ if (checked2.ok) {
379
+ const { text, provenance, usage } = checked2.coaching;
380
+ return {
381
+ case: one,
382
+ ok: true,
383
+ result: {
384
+ text,
385
+ ...provenance !== void 0 ? { provenance } : {},
386
+ ...usage !== void 0 ? { usage } : {}
387
+ },
388
+ coaching: checked2.coaching,
389
+ ms
390
+ };
391
+ }
392
+ return refused(one, raw, checked2.refusal, ms);
393
+ }
265
394
  if (one.request.feature === "writing-feedback") {
266
- const checked2 = _chunkOPMY2MNScjs.checkAiWritingFeedback.call(void 0, raw, one.request);
395
+ const checked2 = _chunkLDFQUZIIcjs.checkAiWritingFeedback.call(void 0, raw, one.request);
267
396
  if (checked2.ok) {
268
397
  const { text, provenance, usage } = checked2.feedback;
269
398
  return {
@@ -280,7 +409,7 @@ async function runCase(ports, one) {
280
409
  }
281
410
  return refused(one, raw, checked2.refusal, ms);
282
411
  }
283
- const checked = one.request.feature === "explanation" ? _chunkOPMY2MNScjs.checkAiExplanation.call(void 0, raw, one.request) : _chunkOPMY2MNScjs.checkAiHint.call(void 0, raw, one.request);
412
+ const checked = one.request.feature === "explanation" ? _chunkLDFQUZIIcjs.checkAiExplanation.call(void 0, raw, one.request) : _chunkLDFQUZIIcjs.checkAiHint.call(void 0, raw, one.request);
284
413
  if (checked.ok) {
285
414
  return { case: one, ok: true, result: checked.result, ms };
286
415
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","../src/ai-check/cases.ts","../src/ai-check/index.ts"],"names":["checked","refused"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","sourcesContent":[null,"import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport { aiWritingFeedbackRequest } from '../ai-writing.js';\nimport type {\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n WrittenResponseData,\n} from '../types/activity.js';\nimport type { AiExplanationRequest, AiHintRequest, AiWritingFeedbackRequest } from '../types/ai.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\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: 'explanation' | 'hint' | 'writing-feedback';\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: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest;\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/** 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\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 ];\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 * });\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 — and\n * `misquotes-answer` on a writing case the next: that prompt corrects words the\n * learner never wrote.\n */\nimport { checkAiExplanation, checkAiHint } from '../ai.js';\nimport { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\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}\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 /** 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];\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 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 === '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"]}
1
+ {"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","../src/ai-check/cases.ts","../src/ai-check/index.ts"],"names":["refused"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","sourcesContent":[null,"import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport { aiCoachingRequest } from '../ai-coaching.js';\nimport { aiWritingFeedbackRequest } from '../ai-writing.js';\nimport { gradeReadAloud } from '../scoring/speech/grade.js';\nimport type {\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n ReadAloudData,\n WrittenResponseData,\n} from '../types/activity.js';\nimport type {\n AiCoachingRequest,\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: 'explanation' | 'hint' | 'writing-feedback' | 'pronunciation-coaching';\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: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest | AiCoachingRequest;\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/** 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 ];\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 * });\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 { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\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}\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 /** 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];\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 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 === '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"]}
@@ -1,4 +1,4 @@
1
- import { k as AiExplanationRequest, q as AiHintRequest, F as AiWritingFeedbackRequest, v as AiRefusal, y as AiTextResult, E as AiWritingFeedback } from './activity-CYc7SD3d.cjs';
1
+ import { o as AiExplanationRequest, u as AiHintRequest, M as AiWritingFeedbackRequest, j as AiCoachingRequest, C as AiRefusal, G as AiTextResult, L as AiWritingFeedback, i as AiCoaching } from './activity-MBOYLBjR.cjs';
2
2
 
3
3
  /**
4
4
  * One call to make against a host's port, and what a learner was looking at
@@ -8,11 +8,11 @@ interface AiCheckCase {
8
8
  /** Stable across releases, so a host can name one in an allow-list or a report. */
9
9
  id: string;
10
10
  /** Which port the case is for. */
11
- feature: 'explanation' | 'hint' | 'writing-feedback';
11
+ feature: 'explanation' | 'hint' | 'writing-feedback' | 'pronunciation-coaching';
12
12
  /** What the learner did, in a few words, for a report a person reads. */
13
13
  about: string;
14
14
  /** The request, built by the SDK exactly as a component builds it. */
15
- request: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest;
15
+ request: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest | AiCoachingRequest;
16
16
  }
17
17
  /**
18
18
  * The cases {@link runAiCheck} runs: every type the SDK explains, answered
@@ -39,6 +39,7 @@ interface AiCheckPorts {
39
39
  explain?(request: AiExplanationRequest): Promise<unknown> | unknown;
40
40
  hint?(request: AiHintRequest): Promise<unknown> | unknown;
41
41
  writingFeedback?(request: AiWritingFeedbackRequest): Promise<unknown> | unknown;
42
+ pronunciationCoaching?(request: AiCoachingRequest): Promise<unknown> | unknown;
42
43
  }
43
44
  /** What happened on one case. */
44
45
  interface AiCheckResult {
@@ -57,6 +58,8 @@ interface AiCheckResult {
57
58
  result?: AiTextResult;
58
59
  /** For a writing case a learner would have been shown: the feedback, as the SDK accepted it. */
59
60
  feedback?: AiWritingFeedback;
61
+ /** For a coaching case a learner would have been shown: the coaching, as the SDK accepted it. */
62
+ coaching?: AiCoaching;
60
63
  /** How long the call took, in milliseconds. */
61
64
  ms: number;
62
65
  }
@@ -1,4 +1,4 @@
1
- import { k as AiExplanationRequest, q as AiHintRequest, F as AiWritingFeedbackRequest, v as AiRefusal, y as AiTextResult, E as AiWritingFeedback } from './activity-CYc7SD3d.js';
1
+ import { o as AiExplanationRequest, u as AiHintRequest, M as AiWritingFeedbackRequest, j as AiCoachingRequest, C as AiRefusal, G as AiTextResult, L as AiWritingFeedback, i as AiCoaching } from './activity-MBOYLBjR.js';
2
2
 
3
3
  /**
4
4
  * One call to make against a host's port, and what a learner was looking at
@@ -8,11 +8,11 @@ interface AiCheckCase {
8
8
  /** Stable across releases, so a host can name one in an allow-list or a report. */
9
9
  id: string;
10
10
  /** Which port the case is for. */
11
- feature: 'explanation' | 'hint' | 'writing-feedback';
11
+ feature: 'explanation' | 'hint' | 'writing-feedback' | 'pronunciation-coaching';
12
12
  /** What the learner did, in a few words, for a report a person reads. */
13
13
  about: string;
14
14
  /** The request, built by the SDK exactly as a component builds it. */
15
- request: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest;
15
+ request: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest | AiCoachingRequest;
16
16
  }
17
17
  /**
18
18
  * The cases {@link runAiCheck} runs: every type the SDK explains, answered
@@ -39,6 +39,7 @@ interface AiCheckPorts {
39
39
  explain?(request: AiExplanationRequest): Promise<unknown> | unknown;
40
40
  hint?(request: AiHintRequest): Promise<unknown> | unknown;
41
41
  writingFeedback?(request: AiWritingFeedbackRequest): Promise<unknown> | unknown;
42
+ pronunciationCoaching?(request: AiCoachingRequest): Promise<unknown> | unknown;
42
43
  }
43
44
  /** What happened on one case. */
44
45
  interface AiCheckResult {
@@ -57,6 +58,8 @@ interface AiCheckResult {
57
58
  result?: AiTextResult;
58
59
  /** For a writing case a learner would have been shown: the feedback, as the SDK accepted it. */
59
60
  feedback?: AiWritingFeedback;
61
+ /** For a coaching case a learner would have been shown: the coaching, as the SDK accepted it. */
62
+ coaching?: AiCoaching;
60
63
  /** How long the call took, in milliseconds. */
61
64
  ms: number;
62
65
  }
package/dist/ai-check.js CHANGED
@@ -1,13 +1,17 @@
1
1
  import {
2
+ aiCoachingRequest,
2
3
  aiExplanationRequest,
3
4
  aiHintRequest,
4
5
  aiWritingFeedbackRequest,
6
+ checkAiCoaching,
5
7
  checkAiExplanation,
6
8
  checkAiHint,
7
9
  checkAiWritingFeedback
8
- } from "./chunk-5UCR4UNR.js";
9
- import "./chunk-Q7MV3IXJ.js";
10
- import "./chunk-D3GP5MET.js";
10
+ } from "./chunk-NQHETFDA.js";
11
+ import {
12
+ gradeReadAloud
13
+ } from "./chunk-IARLJ5MW.js";
14
+ import "./chunk-FBKTION6.js";
11
15
 
12
16
  // src/ai-check/cases.ts
13
17
  var capital = {
@@ -87,6 +91,68 @@ var weekend = {
87
91
  ]
88
92
  }
89
93
  };
94
+ var weather = {
95
+ schemaVersion: "1.0",
96
+ type: "read-aloud",
97
+ id: "ai-check-reading",
98
+ title: "Read the forecast aloud",
99
+ referenceText: "The weather is warm in the north.",
100
+ locale: "en-US",
101
+ recording: { maxSeconds: 20 },
102
+ scoring: {
103
+ dimensions: [
104
+ { name: "accuracy", weight: 2 },
105
+ { name: "fluency", weight: 1 }
106
+ ]
107
+ }
108
+ };
109
+ var said = (text, over = {}) => ({
110
+ text,
111
+ error: "none",
112
+ accuracy: 92,
113
+ ...over
114
+ });
115
+ function forecast(slips) {
116
+ return {
117
+ assessmentVersion: "1.0",
118
+ status: "assessed",
119
+ task: "scripted",
120
+ locale: "en-US",
121
+ referenceText: weather.referenceText,
122
+ recordingKey: "ai-check-take",
123
+ assessor: { kind: "auto", id: "pronunciation-engine" },
124
+ scale: 100,
125
+ scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },
126
+ miscue: "assessor",
127
+ phonemeAlphabet: "ipa",
128
+ words: [
129
+ said("The"),
130
+ slips ? said("weather", {
131
+ accuracy: 38,
132
+ error: "mispronunciation",
133
+ phonemes: [
134
+ { symbol: "w", accuracy: 90 },
135
+ { symbol: "\u025B", accuracy: 85 },
136
+ {
137
+ symbol: "\xF0",
138
+ accuracy: 12,
139
+ heardAs: [
140
+ { symbol: "d", score: 64 },
141
+ { symbol: "z", score: 21 },
142
+ { symbol: "\xF0", score: 12 }
143
+ ]
144
+ },
145
+ { symbol: "\u025A", accuracy: 80 }
146
+ ]
147
+ }) : said("weather"),
148
+ said("is"),
149
+ said("warm"),
150
+ said("in"),
151
+ said("the"),
152
+ slips ? { text: "north", error: "omission" } : said("north")
153
+ ]
154
+ };
155
+ }
90
156
  var chose = (ids) => ({
91
157
  type: "multiple-choice",
92
158
  selectedOptionIds: ids
@@ -131,6 +197,31 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
131
197
  }
132
198
  return { id, feature: "writing-feedback", about, request };
133
199
  }
200
+ function coachingCase(id, about, marks, learnerLocale) {
201
+ const assessment = marks.assessment ?? forecast(true);
202
+ const grade = gradeReadAloud(
203
+ weather,
204
+ { type: "read-aloud", recording: { key: "ai-check-take", mimeType: "audio/wav" } },
205
+ assessment,
206
+ {
207
+ measured: { durationMs: 4e3, voicedMs: 3e3 },
208
+ plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 }
209
+ }
210
+ );
211
+ if ("unscorable" in grade) {
212
+ throw new Error(`ai-check: the reading for "${id}" did not grade: ${grade.reason}`);
213
+ }
214
+ const request = aiCoachingRequest({
215
+ data: weather,
216
+ ...marks.fromGradeOnly === true ? {} : { assessment },
217
+ grade,
218
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
219
+ });
220
+ if (request === null) {
221
+ throw new Error(`ai-check: no coaching request for "${id}"`);
222
+ }
223
+ return { id, feature: "pronunciation-coaching", about, request };
224
+ }
134
225
  function aiCheckCases() {
135
226
  return [
136
227
  explanationCase("mc-correct", "Chose the right city", capital, chose(["madrid"])),
@@ -220,6 +311,23 @@ function aiCheckCases() {
220
311
  "Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.",
221
312
  [],
222
313
  "es"
314
+ ),
315
+ coachingCase("coaching-slips", 'A reading with a mispronounced "th" and a word left out', {
316
+ assessment: forecast(true)
317
+ }),
318
+ coachingCase("coaching-clean", "A reading the engine marked correct throughout", {
319
+ assessment: forecast(false)
320
+ }),
321
+ coachingCase(
322
+ "coaching-from-grade",
323
+ "The same slips, from a stored grade: marks without sounds",
324
+ { fromGradeOnly: true }
325
+ ),
326
+ coachingCase(
327
+ "coaching-in-spanish",
328
+ "The same slips, with coaching asked for in Spanish",
329
+ { assessment: forecast(true) },
330
+ "es"
223
331
  )
224
332
  ];
225
333
  }
@@ -231,7 +339,8 @@ var REFUSALS = [
231
339
  "too-long",
232
340
  "contradicts-grade",
233
341
  "reveals-answer",
234
- "misquotes-answer"
342
+ "misquotes-answer",
343
+ "contradicts-marks"
235
344
  ];
236
345
  function portFor(ports, one) {
237
346
  switch (one.request.feature) {
@@ -239,6 +348,8 @@ function portFor(ports, one) {
239
348
  return ports.explain;
240
349
  case "hint":
241
350
  return ports.hint;
351
+ case "pronunciation-coaching":
352
+ return ports.pronunciationCoaching;
242
353
  default:
243
354
  return ports.writingFeedback;
244
355
  }
@@ -262,6 +373,24 @@ async function runCase(ports, one) {
262
373
  };
263
374
  }
264
375
  const ms = now() - started;
376
+ if (one.request.feature === "pronunciation-coaching") {
377
+ const checked2 = checkAiCoaching(raw, one.request);
378
+ if (checked2.ok) {
379
+ const { text, provenance, usage } = checked2.coaching;
380
+ return {
381
+ case: one,
382
+ ok: true,
383
+ result: {
384
+ text,
385
+ ...provenance !== void 0 ? { provenance } : {},
386
+ ...usage !== void 0 ? { usage } : {}
387
+ },
388
+ coaching: checked2.coaching,
389
+ ms
390
+ };
391
+ }
392
+ return refused(one, raw, checked2.refusal, ms);
393
+ }
265
394
  if (one.request.feature === "writing-feedback") {
266
395
  const checked2 = checkAiWritingFeedback(raw, one.request);
267
396
  if (checked2.ok) {