@intellectif/lk-core 1.4.0 → 1.5.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.
- package/CHANGELOG.md +45 -0
- package/README.md +2 -0
- package/dist/{ai-CnmLPMzd.d.cts → ai-BYryFkF9.d.cts} +199 -5
- package/dist/ai-BYryFkF9.d.cts.map +1 -0
- package/dist/{ai-CnmLPMzd.d.ts → ai-BYryFkF9.d.ts} +199 -5
- package/dist/ai-BYryFkF9.d.ts.map +1 -0
- package/dist/ai-check.cjs +175 -17
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +13 -5
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +13 -5
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +175 -17
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-Cen7-3qs.cjs} +412 -61
- package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
- package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-D8PdhJJz.js} +395 -62
- package/dist/ai-writing-D8PdhJJz.js.map +1 -0
- package/dist/{index-BAfEHCnD.d.cts → index-Bg0aeWtM.d.cts} +327 -3
- package/dist/index-Bg0aeWtM.d.cts.map +1 -0
- package/dist/{index-2nigSf4N.d.cts → index-CxpiWedW.d.ts} +6 -2
- package/dist/index-CxpiWedW.d.ts.map +1 -0
- package/dist/{index-BqMXWuNC.d.cts → index-D7R3Ll8R.d.cts} +2 -2
- package/dist/{index-BqMXWuNC.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
- package/dist/{index-CVWvymVb.d.ts → index-DUaW1Tj5.d.ts} +327 -3
- package/dist/index-DUaW1Tj5.d.ts.map +1 -0
- package/dist/{index-B54baFUA.d.ts → index-qMbxed1B.d.cts} +6 -2
- package/dist/index-qMbxed1B.d.cts.map +1 -0
- package/dist/{index-CcHDTxIZ.d.ts → index-wMc1E_aM.d.ts} +2 -2
- package/dist/{index-CcHDTxIZ.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
- package/dist/index.cjs +31 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -95
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +85 -95
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -117
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
- package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
- package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
- package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
- package/dist/registry-Ce00A_OW.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
- package/dist/registry-DaZRpqjW.js.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
- package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
- package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
- package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
- package/dist/schemas.cjs +2 -2
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +2 -2
- package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
- package/dist/scoring-BKNNTz-s.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
- package/dist/scoring-lKQVYsxy.cjs.map +1 -0
- package/dist/scoring.cjs +8 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-DNzkSJ0S.cjs → xapi-NYrTlpID.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -2
- package/package.json +2 -2
- package/vectors/README.md +24 -1
- package/vectors/scoring.json +18182 -1
- package/dist/ai-CnmLPMzd.d.cts.map +0 -1
- package/dist/ai-CnmLPMzd.d.ts.map +0 -1
- package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
- package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
- package/dist/index-2nigSf4N.d.cts.map +0 -1
- package/dist/index-B54baFUA.d.ts.map +0 -1
- package/dist/index-BAfEHCnD.d.cts.map +0 -1
- package/dist/index-CVWvymVb.d.ts.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.map +0 -1
- package/dist/scoring-B2_HEhMT.cjs.map +0 -1
- package/dist/scoring-BZqUkuDF.js.map +0 -1
package/dist/ai-check.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as AiHintRequest, E as AiExplanationRequest, I as AiGradingJudgement, J as AiRefusal, L as AiGradingRequest, Q as AiTextResult, S as AiDrafts, a as AiCoachingRequest, at as AiWritingFeedbackRequest, i as AiCoaching, it as AiWritingFeedback, l as AiCritique, m as AiCritiqueRequest, w as AiDraftsRequest } from "./ai-BYryFkF9.js";
|
|
2
2
|
//#region src/ai-check/cases.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* One call to make against a host's port, and what a learner was looking at
|
|
@@ -8,17 +8,18 @@ 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' | 'pronunciation-coaching' | 'item-critique' | 'draft-generation';
|
|
11
|
+
feature: 'explanation' | 'hint' | 'writing-feedback' | 'pronunciation-coaching' | 'item-critique' | 'draft-generation' | 'grading';
|
|
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 | AiCoachingRequest | AiCritiqueRequest | AiDraftsRequest;
|
|
15
|
+
request: AiExplanationRequest | AiHintRequest | AiWritingFeedbackRequest | AiCoachingRequest | AiCritiqueRequest | AiDraftsRequest | AiGradingRequest;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* The cases {@link runAiCheck} runs: every type the SDK explains, answered
|
|
19
19
|
* right, wrong and — where a type can be — partly right; every type it hints
|
|
20
|
-
* for, before an answer and after a wrong one;
|
|
21
|
-
* mistakes, without, and revised
|
|
20
|
+
* for, before an answer and after a wrong one; drafts of an essay, with
|
|
21
|
+
* mistakes, without, and revised; and essays to grade, on the task, with
|
|
22
|
+
* mistakes to quote, off the task, and against a passage.
|
|
22
23
|
*
|
|
23
24
|
* They are ordinary calls. What makes them a test is that the SDK knows the
|
|
24
25
|
* answer to each, so it can check what your model writes about it.
|
|
@@ -45,6 +46,8 @@ export interface AiCheckPorts {
|
|
|
45
46
|
critique?(request: AiCritiqueRequest): Promise<unknown> | unknown;
|
|
46
47
|
/** Drafts items from a source, for an author to approve. */
|
|
47
48
|
drafts?(request: AiDraftsRequest): Promise<unknown> | unknown;
|
|
49
|
+
/** Grades a submitted essay on its rubric: a server's port, which no component calls. */
|
|
50
|
+
grading?(request: AiGradingRequest): Promise<unknown> | unknown;
|
|
48
51
|
}
|
|
49
52
|
/** What happened on one case. */
|
|
50
53
|
export interface AiCheckResult {
|
|
@@ -73,6 +76,11 @@ export interface AiCheckResult {
|
|
|
73
76
|
* many came back `complete` and without warnings.
|
|
74
77
|
*/
|
|
75
78
|
drafts?: AiDrafts;
|
|
79
|
+
/**
|
|
80
|
+
* For a grading case the SDK accepted: the judgement, as checked. A reply of
|
|
81
|
+
* the right shape, not a right grade — `gradeFromJudgement` turns it into one.
|
|
82
|
+
*/
|
|
83
|
+
grading?: AiGradingJudgement;
|
|
76
84
|
/** How long the call took, in milliseconds. */
|
|
77
85
|
ms: number;
|
|
78
86
|
}
|
package/dist/ai-check.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-check.d.ts","names":[],"sources":["../src/ai-check/cases.ts","../src/ai-check/index.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"ai-check.d.ts","names":[],"sources":["../src/ai-check/cases.ts","../src/ai-check/index.ts"],"mappings":";;;;;;UAyWiB;;EAEf;;EAEA;;EASA;;EAEA,SACI,uBACA,gBACA,2BACA,oBACA,oBACA,kBACA;;;;;;;;;;;;;;;wBAsOU,gBAAgB;;;;;;;;;;iBCthBf;EACf,SAAS,SAAS,uBAAuB;EACzC,MAAM,SAAS,gBAAgB;EAC/B,iBAAiB,SAAS,2BAA2B;EACrD,uBAAuB,SAAS,oBAAoB;;EAEpD,UAAU,SAAS,oBAAoB;;EAEvC,QAAQ,SAAS,kBAAkB;;EAEnC,SAAS,SAAS,mBAAmB;;;iBAItB;EACf,MAAM;;EAEN;;EAEA,UAAU;;EAEV;;;;;;EAMA,SAAS;;EAET,WAAW;;EAEX,WAAW;;EAEX,WAAW;;;;;;EAMX,SAAS;;;;;EAKT,UAAU;;EAEV;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,WAAW,OAAO;EAClB,SAAS;;iBAGM;;;;;;;EAOf,iBAAiB;;;;;EAKjB;;;;;;;;wBA6JoB,WACpB,OAAO,cACP,UAAS,iBACR,QAAQ;;;;;;;;;wBAwDK,oBAAoB,QAAQ"}
|
package/dist/ai-check.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { o as gradeReadAloud } from "./scoring-
|
|
1
|
+
import { A as checkAiCoaching, B as checkAiExplanation, F as aiExplanationRequest, L as aiHintRequest, V as checkAiHint, _ as aiCritiqueRequest, a as checkAiGrading, d as aiDraftsRequest, f as checkAiDrafts, i as aiGradingRequest, k as aiCoachingRequest, n as checkAiWritingFeedback, t as aiWritingFeedbackRequest, v as checkAiCritique } from "./ai-writing-D8PdhJJz.js";
|
|
2
|
+
import { o as gradeReadAloud } from "./scoring-BKNNTz-s.js";
|
|
3
3
|
//#region src/ai-check/cases.ts
|
|
4
4
|
/**
|
|
5
5
|
* The items the cases are built from. Small, ordinary language-course content
|
|
@@ -16,7 +16,10 @@ import { o as gradeReadAloud } from "./scoring-BZqUkuDF.js";
|
|
|
16
16
|
* twice — so a correction of words nobody wrote is caught;
|
|
17
17
|
* - questions in item groups: a reading passage that prints a question's
|
|
18
18
|
* answer, which a hint must point to without quoting, and a run of blanks
|
|
19
|
-
* whose instructions only the group states
|
|
19
|
+
* whose instructions only the group states;
|
|
20
|
+
* - essays to grade on a rubric of three criteria, two judged on levels and
|
|
21
|
+
* one on a score, so a grade that drops a criterion or leaves a scale is
|
|
22
|
+
* caught.
|
|
20
23
|
*/
|
|
21
24
|
const capital = {
|
|
22
25
|
schemaVersion: "1.0",
|
|
@@ -204,6 +207,117 @@ const weekend = {
|
|
|
204
207
|
}
|
|
205
208
|
] }
|
|
206
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* The weekend task set as an exam question: graded on three criteria, two on
|
|
212
|
+
* levels and one on a score. Three, so a prompt that names only the ones it
|
|
213
|
+
* expects drops one; two scales, so a grade must keep to each.
|
|
214
|
+
*/
|
|
215
|
+
const weekendExam = {
|
|
216
|
+
schemaVersion: "1.0",
|
|
217
|
+
type: "written-response",
|
|
218
|
+
id: "ai-check-writing-exam",
|
|
219
|
+
title: "Your weekend",
|
|
220
|
+
prompt: "Write a few sentences about what you did last weekend.",
|
|
221
|
+
minWords: 15,
|
|
222
|
+
maxWords: 120,
|
|
223
|
+
languageTarget: "en-A2",
|
|
224
|
+
rubric: { criteria: [
|
|
225
|
+
{
|
|
226
|
+
name: "Task",
|
|
227
|
+
description: "Says what the learner did last weekend, in order",
|
|
228
|
+
weight: 1,
|
|
229
|
+
levels: [
|
|
230
|
+
{
|
|
231
|
+
label: "Not met",
|
|
232
|
+
description: "Not about last weekend"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
label: "Partly met",
|
|
236
|
+
description: "About the weekend, but thin or out of order"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
label: "Met",
|
|
240
|
+
description: "Says what they did, in order"
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "Grammar",
|
|
246
|
+
description: "Past tenses, articles, agreement",
|
|
247
|
+
weight: 2,
|
|
248
|
+
levels: [
|
|
249
|
+
{
|
|
250
|
+
label: "Limited",
|
|
251
|
+
description: "Errors in most sentences, some blocking meaning"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
label: "Basic",
|
|
255
|
+
description: "Frequent errors in past tenses and articles"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
label: "Good",
|
|
259
|
+
description: "Some errors, none that blocks meaning"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
label: "Very good",
|
|
263
|
+
description: "Past tenses, articles and agreement almost always right"
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "Vocabulary",
|
|
269
|
+
description: "Everyday words, used correctly",
|
|
270
|
+
weight: 1
|
|
271
|
+
}
|
|
272
|
+
] }
|
|
273
|
+
};
|
|
274
|
+
/** The tides summary set as an exam question, graded against its group's passage. */
|
|
275
|
+
const tidesSummaryExam = {
|
|
276
|
+
schemaVersion: "1.0",
|
|
277
|
+
type: "written-response",
|
|
278
|
+
id: "ai-check-tides-summary-exam",
|
|
279
|
+
title: "Summary",
|
|
280
|
+
prompt: "Summarise the text in one or two sentences.",
|
|
281
|
+
minWords: 10,
|
|
282
|
+
maxWords: 60,
|
|
283
|
+
languageTarget: "en-B1",
|
|
284
|
+
rubric: { criteria: [{
|
|
285
|
+
name: "Content",
|
|
286
|
+
description: "What the text says, and nothing it does not",
|
|
287
|
+
weight: 2,
|
|
288
|
+
levels: [
|
|
289
|
+
{
|
|
290
|
+
label: "Wrong",
|
|
291
|
+
description: "Misstates what the text says"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
label: "Partly right",
|
|
295
|
+
description: "Gets one of its points wrong, or leaves it out"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
label: "Right",
|
|
299
|
+
description: "States its points as the text does"
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}, {
|
|
303
|
+
name: "Language",
|
|
304
|
+
description: "Clear sentences, in the learner's own words",
|
|
305
|
+
weight: 1
|
|
306
|
+
}] }
|
|
307
|
+
};
|
|
308
|
+
const tidesExam = {
|
|
309
|
+
schemaVersion: "1.0",
|
|
310
|
+
type: "item-group",
|
|
311
|
+
id: "ai-check-tides-exam",
|
|
312
|
+
title: "Part 2 — Reading",
|
|
313
|
+
stimulus: {
|
|
314
|
+
id: "ai-check-tides-exam-text",
|
|
315
|
+
kind: "text",
|
|
316
|
+
title: "Tides",
|
|
317
|
+
body: TIDES
|
|
318
|
+
},
|
|
319
|
+
items: [springTides, tidesSummaryExam]
|
|
320
|
+
};
|
|
207
321
|
const weather = {
|
|
208
322
|
schemaVersion: "1.0",
|
|
209
323
|
type: "read-aloud",
|
|
@@ -491,6 +605,21 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale, task
|
|
|
491
605
|
request
|
|
492
606
|
};
|
|
493
607
|
}
|
|
608
|
+
function gradingCase(id, about, text, extra = {}) {
|
|
609
|
+
const built = aiGradingRequest({
|
|
610
|
+
data: extra.data ?? weekendExam,
|
|
611
|
+
response: drafted(text),
|
|
612
|
+
...extra.group ? { group: extra.group } : {},
|
|
613
|
+
...extra.learnerLocale !== void 0 ? { learnerLocale: extra.learnerLocale } : {}
|
|
614
|
+
});
|
|
615
|
+
if (!built.ok) throw new Error(`ai-check: no grading request for "${id}": ${built.problem}`);
|
|
616
|
+
return {
|
|
617
|
+
id,
|
|
618
|
+
feature: "grading",
|
|
619
|
+
about,
|
|
620
|
+
request: built.request
|
|
621
|
+
};
|
|
622
|
+
}
|
|
494
623
|
function coachingCase(id, about, marks, learnerLocale) {
|
|
495
624
|
const assessment = marks.assessment ?? forecast(true);
|
|
496
625
|
const grade = gradeReadAloud(weather, {
|
|
@@ -527,8 +656,9 @@ function coachingCase(id, about, marks, learnerLocale) {
|
|
|
527
656
|
/**
|
|
528
657
|
* The cases {@link runAiCheck} runs: every type the SDK explains, answered
|
|
529
658
|
* right, wrong and — where a type can be — partly right; every type it hints
|
|
530
|
-
* for, before an answer and after a wrong one;
|
|
531
|
-
* mistakes, without, and revised
|
|
659
|
+
* for, before an answer and after a wrong one; drafts of an essay, with
|
|
660
|
+
* mistakes, without, and revised; and essays to grade, on the task, with
|
|
661
|
+
* mistakes to quote, off the task, and against a passage.
|
|
532
662
|
*
|
|
533
663
|
* They are ordinary calls. What makes them a test is that the SDK knows the
|
|
534
664
|
* answer to each, so it can check what your model writes about it.
|
|
@@ -651,7 +781,15 @@ function aiCheckCases() {
|
|
|
651
781
|
locale: "en",
|
|
652
782
|
instructions: "Focus on what Maria and her grandmother buy.",
|
|
653
783
|
settings: { "read-aloud": READ_ALOUD_SETTINGS }
|
|
654
|
-
})
|
|
784
|
+
}),
|
|
785
|
+
gradingCase("grading-on-task", "An essay that does the task, with one slip in its tenses", "Last weekend I went to the market with my sister and we bought some apples. In the afternoon we watched a film at home, and on Sunday I visit my grandmother."),
|
|
786
|
+
gradingCase("grading-mistakes", "An essay with tense, article and agreement mistakes, each to quote as written", "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."),
|
|
787
|
+
gradingCase("grading-off-task", "An essay about something other than last weekend", "My favourite food is pizza. I like it with cheese and tomato, and I eat it every Friday with my family at a small restaurant near our house."),
|
|
788
|
+
gradingCase("grading-summary", "A summary graded against its group's passage, getting the spring tides wrong", "The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.", {
|
|
789
|
+
data: tidesSummaryExam,
|
|
790
|
+
group: tidesExam
|
|
791
|
+
}),
|
|
792
|
+
gradingCase("grading-in-spanish", "The essay with mistakes, with comments asked for in Spanish", "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.", { learnerLocale: "es" })
|
|
655
793
|
];
|
|
656
794
|
}
|
|
657
795
|
//#endregion
|
|
@@ -680,6 +818,7 @@ function aiCheckCases() {
|
|
|
680
818
|
* pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),
|
|
681
819
|
* critique: (request) => callMyModel(CRITIQUE_PROMPT, request),
|
|
682
820
|
* drafts: (request) => callMyModel(DRAFTS_PROMPT, request),
|
|
821
|
+
* grading: (request) => callMyModel(GRADING_PROMPT, request),
|
|
683
822
|
* });
|
|
684
823
|
* console.log(formatAiCheckReport(report));
|
|
685
824
|
* if (report.refused > 0 || report.errors > 0) {
|
|
@@ -692,18 +831,27 @@ function aiCheckCases() {
|
|
|
692
831
|
* to take most seriously — that prompt gives answers away. Next come
|
|
693
832
|
* `misquotes-answer` on a writing case — that prompt corrects words the learner
|
|
694
833
|
* never wrote — and `contradicts-marks` on a coaching case: that prompt coaches
|
|
695
|
-
* words the engine heard as right, or sounds it never reported.
|
|
834
|
+
* words the engine heard as right, or sounds it never reported. On a grading
|
|
835
|
+
* case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one
|
|
836
|
+
* that leaves a criterion out grades a different task, and one off a
|
|
837
|
+
* criterion's scale grades on a scale the author did not write.
|
|
838
|
+
*
|
|
839
|
+
* A grading case the SDK accepts is a reply of the right shape, not a right
|
|
840
|
+
* grade: whether a model grades as your teachers do is what a calibration
|
|
841
|
+
* measures (`calibrationReport`), on your own responses.
|
|
696
842
|
*/
|
|
697
|
-
const REFUSALS =
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
"too-long",
|
|
701
|
-
"contradicts-grade",
|
|
702
|
-
"reveals-answer",
|
|
703
|
-
"misquotes-answer",
|
|
704
|
-
"contradicts-marks",
|
|
705
|
-
"contradicts-item"
|
|
706
|
-
|
|
843
|
+
const REFUSALS = Object.keys({
|
|
844
|
+
malformed: true,
|
|
845
|
+
empty: true,
|
|
846
|
+
"too-long": true,
|
|
847
|
+
"contradicts-grade": true,
|
|
848
|
+
"reveals-answer": true,
|
|
849
|
+
"misquotes-answer": true,
|
|
850
|
+
"contradicts-marks": true,
|
|
851
|
+
"contradicts-item": true,
|
|
852
|
+
"misses-criteria": true,
|
|
853
|
+
"out-of-scale": true
|
|
854
|
+
});
|
|
707
855
|
/** The port a case is for. */
|
|
708
856
|
function portFor(ports, one) {
|
|
709
857
|
switch (one.request.feature) {
|
|
@@ -712,6 +860,7 @@ function portFor(ports, one) {
|
|
|
712
860
|
case "pronunciation-coaching": return ports.pronunciationCoaching;
|
|
713
861
|
case "item-critique": return ports.critique;
|
|
714
862
|
case "draft-generation": return ports.drafts;
|
|
863
|
+
case "grading": return ports.grading;
|
|
715
864
|
default: return ports.writingFeedback;
|
|
716
865
|
}
|
|
717
866
|
}
|
|
@@ -734,6 +883,15 @@ async function runCase(ports, one) {
|
|
|
734
883
|
};
|
|
735
884
|
}
|
|
736
885
|
const ms = now() - started;
|
|
886
|
+
if (one.request.feature === "grading") {
|
|
887
|
+
const checked = checkAiGrading(raw, one.request);
|
|
888
|
+
return checked.ok ? {
|
|
889
|
+
case: one,
|
|
890
|
+
ok: true,
|
|
891
|
+
grading: checked.judgement,
|
|
892
|
+
ms
|
|
893
|
+
} : refused(one, raw, checked.refusal, ms);
|
|
894
|
+
}
|
|
737
895
|
if (one.request.feature === "draft-generation") {
|
|
738
896
|
let next = 0;
|
|
739
897
|
const checked = checkAiDrafts(raw, one.request, { newId: () => {
|
package/dist/ai-check.js.map
CHANGED
|
@@ -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 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
|
+
{"version":3,"file":"ai-check.js","names":[],"sources":["../src/ai-check/cases.ts","../src/ai-check/index.ts"],"sourcesContent":["import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport { aiCoachingRequest } from '../ai-coaching.js';\nimport { aiCritiqueRequest } from '../ai-critique.js';\nimport { aiDraftsRequest } from '../ai-drafts.js';\nimport { aiGradingRequest } from '../ai-grading.js';\nimport { aiWritingFeedbackRequest } from '../ai-writing.js';\nimport { gradeReadAloud } from '../scoring/speech/grade.js';\nimport type {\n ActivityType,\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n ReadAloudData,\n WrittenResponseData,\n} from '../types/activity.js';\nimport type {\n AiCoachingRequest,\n AiCritiqueRequest,\n AiDraftSource,\n AiDraftsRequest,\n AiDraftType,\n AiExplanationRequest,\n AiGradingRequest,\n AiGroupInput,\n AiHintRequest,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport type { ItemGroup } from '../types/item-group.js';\nimport type { SpeechAssessment, SpeechWord } from '../types/speech.js';\n\n/**\n * The items the cases are built from. Small, ordinary language-course content\n * — and deliberately awkward in the places a model slips:\n *\n * - an answer a hint can hardly avoid naming (`Madrid`);\n * - an answer of one short word (`is`), which a hint may write as an ordinary\n * word and must not write beside its neighbour in the passage;\n * - an accented answer (`cañón`), so a hint that spells it without the accent\n * is still caught;\n * - a passage with more than one blank, so a hint can be about the wrong one;\n * - a short essay whose mistakes a model must quote exactly as the learner\n * wrote them — straight apostrophes a model likes to curl, a mistake made\n * twice — so a correction of words nobody wrote is caught;\n * - questions in item groups: a reading passage that prints a question's\n * answer, which a hint must point to without quoting, and a run of blanks\n * whose instructions only the group states;\n * - essays to grade on a rubric of three criteria, two judged on levels and\n * one on a score, so a grade that drops a criterion or leaves a scale is\n * caught.\n */\nconst capital: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-mc',\n title: 'Capital cities',\n question: 'What is the capital of Spain?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'madrid', text: 'Madrid', isCorrect: true },\n { id: 'barcelona', text: 'Barcelona', isCorrect: false },\n { id: 'seville', text: 'Seville', isCorrect: false },\n ],\n};\n\nconst introduction: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-fib',\n title: 'Introducing yourself',\n passage: 'My name {{be}} Rossi and I {{live}} in Madrid.',\n blanks: [\n { id: 'be', acceptedAnswers: ['is'] },\n { id: 'live', acceptedAnswers: ['live'], hint: 'Present simple, first person.' },\n ],\n scoringStrategy: 'partial',\n};\n\nconst landscape: GapSelectData = {\n schemaVersion: '1.0',\n type: 'gap-select',\n id: 'ai-check-gap',\n title: 'Geography',\n locale: 'es-ES',\n passage: 'El río pasa por un {{g1}} muy profundo, y el agua {{g2}} fría.',\n gaps: [\n {\n id: 'g1',\n choices: [\n { id: 'canon', text: 'cañón' },\n { id: 'campo', text: 'campo' },\n ],\n correctChoiceId: 'canon',\n },\n {\n id: 'g2',\n choices: [\n { id: 'esta', text: 'está' },\n { id: 'es', text: 'es' },\n ],\n correctChoiceId: 'esta',\n },\n ],\n scoringStrategy: 'partial',\n};\n\nconst listening: DictationData = {\n schemaVersion: '1.0',\n type: 'dictation',\n id: 'ai-check-dictation',\n title: 'Listen and type',\n transcript: 'The train leaves at half past nine.',\n};\n\n/**\n * A reading group: a passage, a question it answers word for word, and a\n * summary of it. The question's answer is printed in the passage, so a hint\n * that quotes the passage where it answers is refused.\n */\nconst TIDES =\n 'Along most coasts the tide comes in twice a day. It is pulled by the moon: as the Earth ' +\n 'turns, the ocean nearest the moon bulges towards it, and so does the ocean on the far side. ' +\n 'The highest tides, called spring tides, come when the sun and the moon line up.';\n\nconst springTides: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-tides',\n title: 'Spring tides',\n question: 'When do the highest tides come?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'line-up', text: 'When the sun and the moon line up', isCorrect: true },\n { id: 'full-moon', text: 'Only at full moon', isCorrect: false },\n { id: 'winter', text: 'In winter', isCorrect: false },\n ],\n};\n\nconst tidesSummary: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n};\n\nconst tides: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-group',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummary],\n};\n\n/** A part whose questions share instructions the questions themselves never state. */\nconst formation: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-formation',\n title: 'Word formation',\n passage: 'She smiled {{a}} when she heard the news. (HAPPY)',\n blanks: [{ id: 'a', acceptedAnswers: ['happily'] }],\n scoringStrategy: 'all-or-nothing',\n};\n\nconst formationPart: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-formation-part',\n title: 'Part 1 — Word formation',\n stimulus: {\n id: 'ai-check-formation-instructions',\n kind: 'text',\n body: 'Use the word in capitals at the end of each sentence to form a word that fits the gap.',\n },\n items: [formation],\n};\n\nconst weekend: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n { name: 'Grammar', description: 'Past tenses, articles, agreement', weight: 2 },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n { name: 'Task', description: 'Says what the learner did, in order', weight: 1 },\n ],\n },\n};\n\n/**\n * The weekend task set as an exam question: graded on three criteria, two on\n * levels and one on a score. Three, so a prompt that names only the ones it\n * expects drops one; two scales, so a grade must keep to each.\n */\nconst weekendExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-writing-exam',\n title: 'Your weekend',\n prompt: 'Write a few sentences about what you did last weekend.',\n minWords: 15,\n maxWords: 120,\n languageTarget: 'en-A2',\n rubric: {\n criteria: [\n {\n name: 'Task',\n description: 'Says what the learner did last weekend, in order',\n weight: 1,\n levels: [\n { label: 'Not met', description: 'Not about last weekend' },\n { label: 'Partly met', description: 'About the weekend, but thin or out of order' },\n { label: 'Met', description: 'Says what they did, in order' },\n ],\n },\n {\n name: 'Grammar',\n description: 'Past tenses, articles, agreement',\n weight: 2,\n levels: [\n { label: 'Limited', description: 'Errors in most sentences, some blocking meaning' },\n { label: 'Basic', description: 'Frequent errors in past tenses and articles' },\n { label: 'Good', description: 'Some errors, none that blocks meaning' },\n {\n label: 'Very good',\n description: 'Past tenses, articles and agreement almost always right',\n },\n ],\n },\n { name: 'Vocabulary', description: 'Everyday words, used correctly', weight: 1 },\n ],\n },\n};\n\n/** The tides summary set as an exam question, graded against its group's passage. */\nconst tidesSummaryExam: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-tides-summary-exam',\n title: 'Summary',\n prompt: 'Summarise the text in one or two sentences.',\n minWords: 10,\n maxWords: 60,\n languageTarget: 'en-B1',\n rubric: {\n criteria: [\n {\n name: 'Content',\n description: 'What the text says, and nothing it does not',\n weight: 2,\n levels: [\n { label: 'Wrong', description: 'Misstates what the text says' },\n { label: 'Partly right', description: 'Gets one of its points wrong, or leaves it out' },\n { label: 'Right', description: 'States its points as the text does' },\n ],\n },\n { name: 'Language', description: \"Clear sentences, in the learner's own words\", weight: 1 },\n ],\n },\n};\n\nconst tidesExam: ItemGroup = {\n schemaVersion: '1.0',\n type: 'item-group',\n id: 'ai-check-tides-exam',\n title: 'Part 2 — Reading',\n stimulus: { id: 'ai-check-tides-exam-text', kind: 'text', title: 'Tides', body: TIDES },\n items: [springTides, tidesSummaryExam],\n};\n\nconst weather: ReadAloudData = {\n schemaVersion: '1.0',\n type: 'read-aloud',\n id: 'ai-check-reading',\n title: 'Read the forecast aloud',\n referenceText: 'The weather is warm in the north.',\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: {\n dimensions: [\n { name: 'accuracy', weight: 2 },\n { name: 'fluency', weight: 1 },\n ],\n },\n};\n\n/** A word of the forecast as an engine reports it. */\nconst said = (text: string, over: Partial<SpeechWord> = {}): SpeechWord => ({\n text,\n error: 'none',\n accuracy: 92,\n ...over,\n});\n\n/**\n * An engine's assessment of the forecast. With `slips`, \"weather\" is\n * mispronounced — its \"th\" heard as /d/ or /z/ — and \"north\" is left out: the\n * marks a model must coach, and no others.\n */\nfunction forecast(slips: boolean): SpeechAssessment {\n return {\n assessmentVersion: '1.0',\n status: 'assessed',\n task: 'scripted',\n locale: 'en-US',\n referenceText: weather.referenceText,\n recordingKey: 'ai-check-take',\n assessor: { kind: 'auto', id: 'pronunciation-engine' },\n scale: 100,\n scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },\n miscue: 'assessor',\n phonemeAlphabet: 'ipa',\n words: [\n said('The'),\n slips\n ? said('weather', {\n accuracy: 38,\n error: 'mispronunciation',\n phonemes: [\n { symbol: 'w', accuracy: 90 },\n { symbol: 'ɛ', accuracy: 85 },\n {\n symbol: 'ð',\n accuracy: 12,\n heardAs: [\n { symbol: 'd', score: 64 },\n { symbol: 'z', score: 21 },\n { symbol: 'ð', score: 12 },\n ],\n },\n { symbol: 'ɚ', accuracy: 80 },\n ],\n })\n : said('weather'),\n said('is'),\n said('warm'),\n said('in'),\n said('the'),\n slips ? { text: 'north', error: 'omission' } : said('north'),\n ],\n };\n}\n\n/**\n * One call to make against a host's port, and what a learner was looking at\n * when the SDK would have made it.\n */\nexport interface AiCheckCase {\n /** Stable across releases, so a host can name one in an allow-list or a report. */\n id: string;\n /** Which port the case is for. */\n feature:\n | 'explanation'\n | 'hint'\n | 'writing-feedback'\n | 'pronunciation-coaching'\n | 'item-critique'\n | 'draft-generation'\n | 'grading';\n /** What the learner did, in a few words, for a report a person reads. */\n about: string;\n /** The request, built by the SDK exactly as a component builds it. */\n request:\n | AiExplanationRequest\n | AiHintRequest\n | AiWritingFeedbackRequest\n | AiCoachingRequest\n | AiCritiqueRequest\n | AiDraftsRequest\n | AiGradingRequest;\n}\n\nconst chose = (ids: string[]): LearnerResponse => ({\n type: 'multiple-choice',\n selectedOptionIds: ids,\n});\nconst typed = (answers: Record<string, string>): LearnerResponse => ({\n type: 'fill-in-the-blanks',\n answers,\n});\nconst picked = (choices: Record<string, string>): LearnerResponse => ({\n type: 'gap-select',\n selections: choices,\n});\nconst wrote = (text: string): LearnerResponse => ({ type: 'dictation', text });\nconst drafted = (text: string): LearnerResponse => ({\n type: 'written-response',\n text,\n wordCount: 0,\n});\n\n/**\n * Items an author asks a model to review. Each is valid, and each but the last\n * has a flaw a rule cannot see: a distractor that is botanically a right\n * answer, and a task far above the level it is set for.\n */\nconst fruit: MultipleChoiceData = {\n schemaVersion: '1.0',\n type: 'multiple-choice',\n id: 'ai-check-fruit',\n title: 'Food words',\n question: 'Which of these is a fruit?',\n mode: 'single',\n scoringStrategy: 'all-or-nothing',\n options: [\n { id: 'f1', text: 'Apple', isCorrect: true },\n { id: 'f2', text: 'Carrot', isCorrect: false },\n { id: 'f3', text: 'Tomato', isCorrect: false },\n ],\n};\n\nconst tooHard: WrittenResponseData = {\n schemaVersion: '1.0',\n type: 'written-response',\n id: 'ai-check-too-hard',\n title: 'Cities',\n prompt: 'Discuss the socioeconomic ramifications of rapid urbanisation in developing economies.',\n minWords: 40,\n maxWords: 80,\n};\n\nconst cleanCloze: FillInTheBlanksData = {\n schemaVersion: '1.0',\n type: 'fill-in-the-blanks',\n id: 'ai-check-clean-cloze',\n title: 'Daily routine',\n passage: 'Every morning I {{1}} up at seven and {{2}} a shower.',\n blanks: [\n { id: '1', acceptedAnswers: ['get', 'wake'] },\n { id: '2', acceptedAnswers: ['take', 'have'] },\n ],\n scoringStrategy: 'partial',\n};\n\nfunction critiqueCase(\n id: string,\n about: string,\n type: ActivityType,\n draft: unknown,\n extra: { level?: string; authorLocale?: string } = {},\n): AiCheckCase {\n const request = aiCritiqueRequest({ type, draft, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no critique request for \"${id}\"`);\n }\n return { id, feature: 'item-critique', about, request };\n}\n\n/** A short reading to draft from: plain, and with facts a question can be asked about. */\nconst MARKET =\n 'Maria lives in Seville, in the south of Spain. Every Saturday she walks to the market with her ' +\n 'grandmother. They buy oranges, fresh bread and a little cheese. On the way home they stop at a ' +\n 'café, where Maria drinks hot chocolate and her grandmother reads the newspaper.';\n\n/** The same reading as a video's captions, with times in seconds. */\nconst MARKET_CAPTIONS = [\n { start: 0, end: 4.2, text: 'Maria lives in Seville, in the south of Spain.' },\n { start: 4.2, end: 8.9, text: 'Every Saturday she walks to the market with her grandmother.' },\n { start: 8.9, end: 12.6, text: 'They buy oranges, fresh bread and a little cheese.' },\n { start: 12.6, end: 18.1, text: 'On the way home they stop at a café.' },\n];\n\nfunction draftsCase(\n id: string,\n about: string,\n types: AiDraftType[],\n source: AiDraftSource,\n extra: Omit<Parameters<typeof aiDraftsRequest>[0], 'types' | 'source'> = {},\n): AiCheckCase {\n const request = aiDraftsRequest({ types, source, ...extra });\n if (request === null) {\n throw new Error(`ai-check: no drafts request for \"${id}\"`);\n }\n return { id, feature: 'draft-generation', about, request };\n}\n\n/**\n * What a host sets on every read-aloud it drafts: the SDK has no default for a\n * recording's length or the dimensions a reading is graded on.\n */\nconst READ_ALOUD_SETTINGS = {\n locale: 'en-US',\n recording: { maxSeconds: 20 },\n scoring: { dimensions: [{ name: 'accuracy', weight: 1 }] },\n};\n\n/** Builds a case, failing loudly rather than shipping a case with no request. */\nfunction explanationCase(\n id: string,\n about: string,\n data: Parameters<typeof aiExplanationRequest>[0]['data'],\n response: LearnerResponse,\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiExplanationRequest({ data, response, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no explanation request for \"${id}\"`);\n }\n return { id, feature: 'explanation', about, request };\n}\n\nfunction hintCase(\n id: string,\n about: string,\n data: Parameters<typeof aiHintRequest>[0]['data'],\n response: LearnerResponse,\n previousHints: string[] = [],\n group?: AiGroupInput,\n): AiCheckCase {\n const request = aiHintRequest({ data, response, previousHints, ...(group ? { group } : {}) });\n if (request === null) {\n throw new Error(`ai-check: no hint request for \"${id}\"`);\n }\n return { id, feature: 'hint', about, request };\n}\n\nfunction writingCase(\n id: string,\n about: string,\n text: string,\n previousFeedback: string[] = [],\n learnerLocale?: string,\n task: { data: WrittenResponseData; group?: AiGroupInput } = { data: weekend },\n): AiCheckCase {\n const request = aiWritingFeedbackRequest({\n data: task.data,\n response: drafted(text),\n previousFeedback,\n ...(task.group ? { group: task.group } : {}),\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no writing feedback request for \"${id}\"`);\n }\n return { id, feature: 'writing-feedback', about, request };\n}\n\nfunction gradingCase(\n id: string,\n about: string,\n text: string,\n extra: { data?: WrittenResponseData; group?: AiGroupInput; learnerLocale?: string } = {},\n): AiCheckCase {\n const built = aiGradingRequest({\n data: extra.data ?? weekendExam,\n response: drafted(text),\n ...(extra.group ? { group: extra.group } : {}),\n ...(extra.learnerLocale !== undefined ? { learnerLocale: extra.learnerLocale } : {}),\n });\n if (!built.ok) {\n throw new Error(`ai-check: no grading request for \"${id}\": ${built.problem}`);\n }\n return { id, feature: 'grading', about, request: built.request };\n}\n\nfunction coachingCase(\n id: string,\n about: string,\n marks: { assessment?: SpeechAssessment; fromGradeOnly?: boolean },\n learnerLocale?: string,\n): AiCheckCase {\n const assessment = marks.assessment ?? forecast(true);\n const grade = gradeReadAloud(\n weather,\n { type: 'read-aloud', recording: { key: 'ai-check-take', mimeType: 'audio/wav' } },\n assessment,\n {\n measured: { durationMs: 4000, voicedMs: 3000 },\n plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 },\n },\n );\n if ('unscorable' in grade) {\n throw new Error(`ai-check: the reading for \"${id}\" did not grade: ${grade.reason}`);\n }\n const request = aiCoachingRequest({\n data: weather,\n ...(marks.fromGradeOnly === true ? {} : { assessment }),\n grade,\n ...(learnerLocale !== undefined ? { learnerLocale } : {}),\n });\n if (request === null) {\n throw new Error(`ai-check: no coaching request for \"${id}\"`);\n }\n return { id, feature: 'pronunciation-coaching', about, request };\n}\n\n/**\n * The cases {@link runAiCheck} runs: every type the SDK explains, answered\n * right, wrong and — where a type can be — partly right; every type it hints\n * for, before an answer and after a wrong one; drafts of an essay, with\n * mistakes, without, and revised; and essays to grade, on the task, with\n * mistakes to quote, off the task, and against a passage.\n *\n * They are ordinary calls. What makes them a test is that the SDK knows the\n * answer to each, so it can check what your model writes about it.\n *\n * A fresh array each call, so a caller may filter, slice or add to it without\n * changing what the next caller gets.\n */\nexport function aiCheckCases(): AiCheckCase[] {\n return [\n explanationCase('mc-correct', 'Chose the right city', capital, chose(['madrid'])),\n explanationCase('mc-wrong', 'Chose Barcelona', capital, chose(['barcelona'])),\n explanationCase('mc-blank', 'Chose nothing and submitted', capital, chose([])),\n explanationCase(\n 'fib-correct',\n 'Filled both blanks correctly',\n introduction,\n typed({ be: 'is', live: 'live' }),\n ),\n explanationCase(\n 'fib-partly',\n 'One blank right, one wrong',\n introduction,\n typed({ be: 'is', live: 'lives' }),\n ),\n explanationCase(\n 'fib-wrong',\n 'Both blanks wrong',\n introduction,\n typed({ be: 'are', live: 'living' }),\n ),\n explanationCase(\n 'gap-partly',\n 'One gap right, one wrong, in Spanish',\n landscape,\n picked({ g1: 'canon', g2: 'es' }),\n ),\n explanationCase(\n 'gap-wrong',\n 'Both gaps wrong, in Spanish',\n landscape,\n picked({ g1: 'campo', g2: 'es' }),\n ),\n explanationCase(\n 'dictation-near',\n 'Typed the sentence with two words wrong',\n listening,\n wrote('The train leaves at half past five'),\n ),\n explanationCase(\n 'dictation-correct',\n 'Typed the sentence correctly',\n listening,\n wrote('The train leaves at half past nine.'),\n ),\n explanationCase(\n 'mc-reading-wrong',\n \"Chose 'Only at full moon', on a question about its group's passage\",\n springTides,\n chose(['full-moon']),\n tides,\n ),\n hintCase('mc-hint-1', 'Has not answered yet', capital, chose([])),\n hintCase('mc-hint-2', 'Asked again after choosing Seville', capital, chose(['seville']), [\n 'Think about where the government sits.',\n ]),\n hintCase('fib-hint-1', 'Both blanks empty', introduction, typed({})),\n hintCase(\n 'fib-hint-2',\n 'Asked again with one blank wrong',\n introduction,\n typed({ be: 'are', live: '' }),\n ['Look again at the first blank.'],\n ),\n hintCase('gap-hint-1', 'Neither gap chosen, in Spanish', landscape, picked({})),\n hintCase(\n 'gap-hint-2',\n 'Asked again with the first gap wrong',\n landscape,\n picked({ g1: 'campo' }),\n ['Piensa en la forma del terreno.'],\n ),\n hintCase(\n 'mc-reading-hint',\n \"Has not answered a question whose answer its group's passage prints\",\n springTides,\n chose([]),\n [],\n tides,\n ),\n hintCase(\n 'fib-formation-hint',\n 'A word-formation blank, under the instructions its group gives',\n formation,\n typed({}),\n [],\n formationPart,\n ),\n writingCase(\n 'writing-mistakes',\n 'A first draft with tense, article and agreement mistakes',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n writingCase(\n 'writing-clean',\n 'A first draft with no mistakes to correct',\n 'Last weekend I went to the market and bought an apple. My friends were there too, so we stayed until the evening. Then we went to the cinema.',\n ),\n writingCase(\n 'writing-revised',\n 'A second draft that fixed some mistakes',\n \"Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.\",\n ['Check your past tenses: \"go\" and \"buyed\" should be past forms.'],\n ),\n writingCase(\n 'writing-feedback-in-spanish',\n 'A first draft, with feedback asked for in Spanish',\n 'Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.',\n [],\n 'es',\n ),\n writingCase(\n 'writing-summary',\n \"A summary of its group's passage that gets the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n [],\n undefined,\n { data: tidesSummary, group: tides },\n ),\n coachingCase('coaching-slips', 'A reading with a mispronounced \"th\" and a word left out', {\n assessment: forecast(true),\n }),\n coachingCase('coaching-clean', 'A reading the engine marked correct throughout', {\n assessment: forecast(false),\n }),\n coachingCase(\n 'coaching-from-grade',\n 'The same slips, from a stored grade: marks without sounds',\n { fromGradeOnly: true },\n ),\n coachingCase(\n 'coaching-in-spanish',\n 'The same slips, with coaching asked for in Spanish',\n { assessment: forecast(true) },\n 'es',\n ),\n critiqueCase(\n 'critique-second-answer',\n 'A question whose distractor \"Tomato\" is botanically a fruit',\n 'multiple-choice',\n fruit,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-level',\n 'A writing task far above the A1 level it is set for',\n 'written-response',\n tooHard,\n { level: 'A1' },\n ),\n critiqueCase(\n 'critique-clean',\n 'A cloze with nothing wrong in it',\n 'fill-in-the-blanks',\n cleanCloze,\n {\n level: 'A2',\n },\n ),\n critiqueCase(\n 'critique-in-spanish',\n 'The fruit question, reviewed in Spanish for its author',\n 'multiple-choice',\n fruit,\n { level: 'A1', authorLocale: 'es' },\n ),\n draftsCase(\n 'drafts-questions',\n 'Four multiple-choice questions on a short reading',\n ['multiple-choice'],\n { kind: 'passage', text: MARKET },\n { count: 4, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-cloze',\n 'A cloze on the same reading, from its transcript',\n ['fill-in-the-blanks'],\n { kind: 'transcript', text: MARKET },\n { count: 1, level: 'A2', locale: 'en', instructions: 'Blank the verbs.' },\n ),\n draftsCase(\n 'drafts-gaps',\n 'A choose-the-word item on the same reading',\n ['gap-select'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-writing',\n 'A writing task that follows on from the reading',\n ['written-response'],\n { kind: 'passage', text: MARKET },\n { count: 1, level: 'A2', locale: 'en' },\n ),\n draftsCase(\n 'drafts-video',\n 'A video quiz from timed captions: multiple choice, then dictation, then read-aloud, as many as the video is worth',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'captions', cues: MARKET_CAPTIONS },\n { level: 'A2', locale: 'en', settings: { 'read-aloud': READ_ALOUD_SETTINGS } },\n ),\n draftsCase(\n 'drafts-script',\n 'The same video from its script alone, with no times, and what the author asked for',\n ['multiple-choice', 'dictation', 'read-aloud'],\n { kind: 'transcript', text: MARKET },\n {\n level: 'A2',\n locale: 'en',\n instructions: 'Focus on what Maria and her grandmother buy.',\n settings: { 'read-aloud': READ_ALOUD_SETTINGS },\n },\n ),\n gradingCase(\n 'grading-on-task',\n 'An essay that does the task, with one slip in its tenses',\n 'Last weekend I went to the market with my sister and we bought some apples. In the afternoon we watched a film at home, and on Sunday I visit my grandmother.',\n ),\n gradingCase(\n 'grading-mistakes',\n 'An essay with tense, article and agreement mistakes, each to quote as written',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n ),\n gradingCase(\n 'grading-off-task',\n 'An essay about something other than last weekend',\n 'My favourite food is pizza. I like it with cheese and tomato, and I eat it every Friday with my family at a small restaurant near our house.',\n ),\n gradingCase(\n 'grading-summary',\n \"A summary graded against its group's passage, getting the spring tides wrong\",\n 'The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.',\n { data: tidesSummaryExam, group: tidesExam },\n ),\n gradingCase(\n 'grading-in-spanish',\n 'The essay with mistakes, with comments asked for in Spanish',\n \"Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.\",\n { learnerLocale: 'es' },\n ),\n ];\n}\n","/**\n * A test kit for the prompts behind your AI ports.\n *\n * The SDK cannot test your model, and will not try. What it can do is make the\n * calls a learner's questions would make — on items whose answers it knows —\n * and run the same checks it runs before a learner sees anything. What comes\n * back is a count of what would have been shown and what would have been\n * refused, and why.\n *\n * Run it where you change a prompt or a model, in your own CI, with your own\n * key. It is a separate entry point (`@intellectif/lk-core/ai-check`) because\n * it is for your test run, not for a learner's browser: nothing here is\n * imported by anything a page renders.\n *\n * ```ts\n * import { aiCheckCases, formatAiCheckReport, runAiCheck } from '@intellectif/lk-core/ai-check';\n *\n * const report = await runAiCheck({\n * explain: (request) => callMyModel(EXPLAIN_PROMPT, request),\n * hint: (request) => callMyModel(HINT_PROMPT, request),\n * writingFeedback: (request) => callMyModel(FEEDBACK_PROMPT, request),\n * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),\n * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),\n * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),\n * grading: (request) => callMyModel(GRADING_PROMPT, request),\n * });\n * console.log(formatAiCheckReport(report));\n * if (report.refused > 0 || report.errors > 0) {\n * process.exitCode = 1;\n * }\n * ```\n *\n * A refusal is a real failure, not a warning: it is an answer a learner would\n * have asked for and not received. `reveals-answer` on a hint case is the one\n * to take most seriously — that prompt gives answers away. Next come\n * `misquotes-answer` on a writing case — that prompt corrects words the learner\n * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches\n * words the engine heard as right, or sounds it never reported. On a grading\n * case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one\n * that leaves a criterion out grades a different task, and one off a\n * criterion's scale grades on a scale the author did not write.\n *\n * A grading case the SDK accepts is a reply of the right shape, not a right\n * grade: whether a model grades as your teachers do is what a calibration\n * measures (`calibrationReport`), on your own responses.\n */\nimport { checkAiExplanation, checkAiHint } from '../ai.js';\nimport { checkAiCoaching } from '../ai-coaching.js';\nimport { checkAiCritique } from '../ai-critique.js';\nimport { checkAiDrafts } from '../ai-drafts.js';\nimport { checkAiGrading } from '../ai-grading.js';\nimport { checkAiWritingFeedback } from '../ai-writing.js';\nimport type {\n AiCoaching,\n AiCoachingRequest,\n AiCritique,\n AiCritiqueRequest,\n AiDrafts,\n AiDraftsRequest,\n AiExplanationRequest,\n AiGradingJudgement,\n AiGradingRequest,\n AiHintRequest,\n AiRefusal,\n AiTextResult,\n AiWritingFeedback,\n AiWritingFeedbackRequest,\n} from '../types/ai.js';\nimport { type AiCheckCase, aiCheckCases } from './cases.js';\n\nexport { type AiCheckCase, aiCheckCases } from './cases.js';\n\n/**\n * Your ports, as this kit calls them. The same functions\n * `<LkAiProvider ai={…}>` takes, minus the `AbortSignal` a browser passes —\n * nothing here abandons a call.\n *\n * Leave one out and its cases are skipped rather than failed.\n */\nexport interface AiCheckPorts {\n explain?(request: AiExplanationRequest): Promise<unknown> | unknown;\n hint?(request: AiHintRequest): Promise<unknown> | unknown;\n writingFeedback?(request: AiWritingFeedbackRequest): Promise<unknown> | unknown;\n pronunciationCoaching?(request: AiCoachingRequest): Promise<unknown> | unknown;\n /** Reviews an item for its author: the port an editor's \"Review with AI\" calls. */\n critique?(request: AiCritiqueRequest): Promise<unknown> | unknown;\n /** Drafts items from a source, for an author to approve. */\n drafts?(request: AiDraftsRequest): Promise<unknown> | unknown;\n /** Grades a submitted essay on its rubric: a server's port, which no component calls. */\n grading?(request: AiGradingRequest): Promise<unknown> | unknown;\n}\n\n/** What happened on one case. */\nexport interface AiCheckResult {\n case: AiCheckCase;\n /** Whether a learner would have been shown what came back. */\n ok: boolean;\n /** Why it would not have been shown. */\n refusal?: AiRefusal;\n /** The port threw or rejected. Its message, as a string. */\n error?: string;\n /**\n * What came back, once the SDK had read it — so a refusal can be read beside\n * the text that caused it. Absent when the port failed or answered with\n * nothing the SDK could read.\n */\n result?: AiTextResult;\n /** For a writing case a learner would have been shown: the feedback, as the SDK accepted it. */\n feedback?: AiWritingFeedback;\n /** For a coaching case a learner would have been shown: the coaching, as the SDK accepted it. */\n coaching?: AiCoaching;\n /** For a critique case an author would have been shown: the findings, as the SDK accepted them. */\n critique?: AiCritique;\n /**\n * For a drafts case the SDK read: the drafts, each with what `validateDraft`\n * and the item critic say of it. A reply read is not a good one — check how\n * many came back `complete` and without warnings.\n */\n drafts?: AiDrafts;\n /**\n * For a grading case the SDK accepted: the judgement, as checked. A reply of\n * the right shape, not a right grade — `gradeFromJudgement` turns it into one.\n */\n grading?: AiGradingJudgement;\n /** How long the call took, in milliseconds. */\n ms: number;\n}\n\n/** What a run came to. */\nexport interface AiCheckReport {\n /** Cases run: the ones a port was supplied for. */\n total: number;\n /** Cases whose answer a learner would have been shown. */\n shown: number;\n /** Cases whose answer the SDK refused. */\n refused: number;\n /** Cases where the port itself failed. */\n errors: number;\n /** Cases skipped because no port was supplied for that feature. */\n skipped: number;\n /** How many refusals of each reason. */\n byRefusal: Record<AiRefusal, number>;\n results: AiCheckResult[];\n}\n\nexport interface AiCheckOptions {\n /**\n * The cases to run. Defaults to {@link aiCheckCases}. Pass a filtered list to\n * run one type, or your own cases — built with `aiExplanationRequest` and\n * `aiHintRequest` on your own items, which is how you test a prompt against\n * the content your learners actually see.\n */\n cases?: readonly AiCheckCase[];\n /**\n * How many cases to have in flight at once. Default 1: a run in CI hits a\n * rate limit far more often than it runs out of patience.\n */\n concurrency?: number;\n}\n\n/**\n * Every refusal, in the order a report lists them. A map rather than a list, so\n * a refusal added to `AiRefusal` and not here fails to compile instead of going\n * uncounted.\n */\nconst REFUSAL_ORDER = {\n malformed: true,\n empty: true,\n 'too-long': true,\n 'contradicts-grade': true,\n 'reveals-answer': true,\n 'misquotes-answer': true,\n 'contradicts-marks': true,\n 'contradicts-item': true,\n 'misses-criteria': true,\n 'out-of-scale': true,\n} as const satisfies Record<AiRefusal, true>;\n\nconst REFUSALS = Object.keys(REFUSAL_ORDER) as AiRefusal[];\n\n/** The port a case is for. */\nfunction portFor(\n ports: AiCheckPorts,\n one: AiCheckCase,\n): ((request: never) => Promise<unknown> | unknown) | undefined {\n switch (one.request.feature) {\n case 'explanation':\n return ports.explain as never;\n case 'hint':\n return ports.hint as never;\n case 'pronunciation-coaching':\n return ports.pronunciationCoaching as never;\n case 'item-critique':\n return ports.critique as never;\n case 'draft-generation':\n return ports.drafts as never;\n case 'grading':\n return ports.grading as never;\n default:\n return ports.writingFeedback as never;\n }\n}\n\n/** Milliseconds, from whichever clock this runtime has. */\nconst now = (): number => Date.now();\n\n/** Runs one case: calls the port, times it, and checks what came back. */\nasync function runCase(ports: AiCheckPorts, one: AiCheckCase): Promise<AiCheckResult> {\n const started = now();\n const port = portFor(ports, one);\n if (port === undefined) {\n throw new Error(`ai-check: no port for \"${one.id}\"`);\n }\n let raw: unknown;\n try {\n raw = await port(one.request as never);\n } catch (error) {\n return {\n case: one,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n ms: now() - started,\n };\n }\n const ms = now() - started;\n if (one.request.feature === 'grading') {\n const checked = checkAiGrading(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, grading: checked.judgement, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'draft-generation') {\n let next = 0;\n const checked = checkAiDrafts(raw, one.request, {\n newId: () => {\n next += 1;\n return `ai-check-${next}`;\n },\n });\n return checked.ok\n ? { case: one, ok: true, drafts: checked.drafts, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'item-critique') {\n const checked = checkAiCritique(raw, one.request);\n return checked.ok\n ? { case: one, ok: true, critique: checked.critique, ms }\n : refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'pronunciation-coaching') {\n const checked = checkAiCoaching(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.coaching;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n coaching: checked.coaching,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n if (one.request.feature === 'writing-feedback') {\n const checked = checkAiWritingFeedback(raw, one.request);\n if (checked.ok) {\n const { text, provenance, usage } = checked.feedback;\n return {\n case: one,\n ok: true,\n result: {\n text,\n ...(provenance !== undefined ? { provenance } : {}),\n ...(usage !== undefined ? { usage } : {}),\n },\n feedback: checked.feedback,\n ms,\n };\n }\n return refused(one, raw, checked.refusal, ms);\n }\n const checked =\n one.request.feature === 'explanation'\n ? checkAiExplanation(raw, one.request)\n : checkAiHint(raw, one.request);\n if (checked.ok) {\n return { case: one, ok: true, result: checked.result, ms };\n }\n return refused(one, raw, checked.refusal, ms);\n}\n\n/** A case whose answer was refused, with the text that came back when there was one to read. */\nfunction refused(one: AiCheckCase, raw: unknown, refusal: AiRefusal, ms: number): AiCheckResult {\n const text =\n typeof (raw as { text?: unknown })?.text === 'string' ? (raw as AiTextResult) : undefined;\n return {\n case: one,\n ok: false,\n refusal,\n ...(text !== undefined ? { result: text } : {}),\n ms,\n };\n}\n\n/**\n * Calls your ports on every case and reports what a learner would have seen.\n *\n * It never throws for a failing case: a port that rejects is an `error` in the\n * report, so one bad call cannot hide the rest.\n */\nexport async function runAiCheck(\n ports: AiCheckPorts,\n options: AiCheckOptions = {},\n): Promise<AiCheckReport> {\n const all = options.cases ?? aiCheckCases();\n const asked = Math.trunc(options.concurrency ?? 1);\n const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;\n const runnable = all.filter((one) => portFor(ports, one) !== undefined);\n const results: AiCheckResult[] = new Array(runnable.length);\n let next = 0;\n const lane = async (): Promise<void> => {\n while (next < runnable.length) {\n const mine = next;\n next += 1;\n const one = runnable[mine];\n if (one === undefined) {\n return;\n }\n results[mine] = await runCase(ports, one);\n }\n };\n await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));\n\n const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0])) as Record<\n AiRefusal,\n number\n >;\n let shown = 0;\n let refused = 0;\n let errors = 0;\n for (const result of results) {\n if (result.ok) {\n shown += 1;\n } else if (result.refusal !== undefined) {\n refused += 1;\n byRefusal[result.refusal] += 1;\n } else {\n errors += 1;\n }\n }\n return {\n total: results.length,\n shown,\n refused,\n errors,\n skipped: all.length - runnable.length,\n byRefusal,\n results,\n };\n}\n\n/**\n * The report as a few lines for a CI log: the counts, then every case that\n * failed, with what came back.\n *\n * The refused text is printed — this is your own test run, not a learner's\n * screen, and a hint that gives the answer away is only fixable if you can\n * read it.\n */\nexport function formatAiCheckReport(report: AiCheckReport): string {\n const lines: string[] = [];\n const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);\n lines.push(\n `ai-check: ${report.shown}/${report.total} shown, ${report.refused} refused, ${report.errors} failed` +\n `${report.skipped > 0 ? `, ${report.skipped} skipped (no port)` : ''} — slowest ${slowest} ms`,\n );\n for (const reason of REFUSALS) {\n const count = report.byRefusal[reason];\n if (count > 0) {\n lines.push(` ${reason}: ${count}`);\n }\n }\n for (const result of report.results) {\n if (result.ok) {\n continue;\n }\n const why = result.refusal ?? `port failed (${result.error ?? 'no message'})`;\n lines.push(` ✗ ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);\n if (result.result !== undefined) {\n lines.push(` ${result.result.text.replace(/\\s+/g, ' ').slice(0, 200)}`);\n }\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|