@intellectif/lk-core 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/ai-B222dtHH.d.cts +2154 -0
  4. package/dist/ai-B222dtHH.d.cts.map +1 -0
  5. package/dist/ai-B222dtHH.d.ts +2154 -0
  6. package/dist/ai-B222dtHH.d.ts.map +1 -0
  7. package/dist/ai-check.cjs +758 -464
  8. package/dist/ai-check.cjs.map +1 -1
  9. package/dist/ai-check.d.cts +82 -68
  10. package/dist/ai-check.d.cts.map +1 -0
  11. package/dist/ai-check.d.ts +82 -68
  12. package/dist/ai-check.d.ts.map +1 -0
  13. package/dist/ai-check.js +756 -465
  14. package/dist/ai-check.js.map +1 -1
  15. package/dist/ai-writing-CCBjTOM3.js +2496 -0
  16. package/dist/ai-writing-CCBjTOM3.js.map +1 -0
  17. package/dist/ai-writing-DCVkv4Zj.cjs +2735 -0
  18. package/dist/ai-writing-DCVkv4Zj.cjs.map +1 -0
  19. package/dist/{index-DlcqkM_L.d.ts → index-BWPRPIXL.d.ts} +263 -258
  20. package/dist/index-BWPRPIXL.d.ts.map +1 -0
  21. package/dist/index-CXyOV8sm.d.cts +153 -0
  22. package/dist/index-CXyOV8sm.d.cts.map +1 -0
  23. package/dist/index-CegM8o15.d.ts +153 -0
  24. package/dist/index-CegM8o15.d.ts.map +1 -0
  25. package/dist/index-Dyu66R4-.d.cts +1071 -0
  26. package/dist/index-Dyu66R4-.d.cts.map +1 -0
  27. package/dist/{index-DlCW0XaQ.d.cts → index-O4_ExlAD.d.cts} +263 -258
  28. package/dist/index-O4_ExlAD.d.cts.map +1 -0
  29. package/dist/index-UGxRSopE.d.ts +1071 -0
  30. package/dist/index-UGxRSopE.d.ts.map +1 -0
  31. package/dist/index.cjs +1210 -1671
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +1090 -853
  34. package/dist/index.d.cts.map +1 -0
  35. package/dist/index.d.ts +1090 -853
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1063 -1672
  38. package/dist/index.js.map +1 -1
  39. package/dist/item-group-DXiR8nOr.js +445 -0
  40. package/dist/item-group-DXiR8nOr.js.map +1 -0
  41. package/dist/item-group-DroFXgLb.cjs +505 -0
  42. package/dist/item-group-DroFXgLb.cjs.map +1 -0
  43. package/dist/registry-DVi0LR_i.cjs +5915 -0
  44. package/dist/registry-DVi0LR_i.cjs.map +1 -0
  45. package/dist/registry-Dsx9mNsS.js +5449 -0
  46. package/dist/registry-Dsx9mNsS.js.map +1 -0
  47. package/dist/schemas-Bdw0nGPo.js +152 -0
  48. package/dist/schemas-Bdw0nGPo.js.map +1 -0
  49. package/dist/schemas-DVbAOrtd.cjs +224 -0
  50. package/dist/schemas-DVbAOrtd.cjs.map +1 -0
  51. package/dist/schemas.cjs +16 -32
  52. package/dist/schemas.d.cts +2 -2
  53. package/dist/schemas.d.ts +2 -2
  54. package/dist/schemas.js +3 -32
  55. package/dist/scoring-B2_HEhMT.cjs +1748 -0
  56. package/dist/scoring-B2_HEhMT.cjs.map +1 -0
  57. package/dist/scoring-BZqUkuDF.js +1604 -0
  58. package/dist/scoring-BZqUkuDF.js.map +1 -0
  59. package/dist/scoring.cjs +37 -75
  60. package/dist/scoring.d.cts +3 -1103
  61. package/dist/scoring.d.ts +3 -1103
  62. package/dist/scoring.js +3 -75
  63. package/dist/xapi-CMTph7wG.js +332 -0
  64. package/dist/xapi-CMTph7wG.js.map +1 -0
  65. package/dist/xapi-DNzkSJ0S.cjs +362 -0
  66. package/dist/xapi-DNzkSJ0S.cjs.map +1 -0
  67. package/dist/xapi.cjs +7 -16
  68. package/dist/xapi.d.cts +2 -150
  69. package/dist/xapi.d.ts +2 -150
  70. package/dist/xapi.js +2 -16
  71. package/package.json +4 -4
  72. package/dist/activity-MBOYLBjR.d.cts +0 -1840
  73. package/dist/activity-MBOYLBjR.d.ts +0 -1840
  74. package/dist/chunk-2JT2WUQ2.cjs +0 -458
  75. package/dist/chunk-2JT2WUQ2.cjs.map +0 -1
  76. package/dist/chunk-DSDXL3JO.cjs +0 -4597
  77. package/dist/chunk-DSDXL3JO.cjs.map +0 -1
  78. package/dist/chunk-FBKTION6.js +0 -4597
  79. package/dist/chunk-FBKTION6.js.map +0 -1
  80. package/dist/chunk-IARLJ5MW.js +0 -1256
  81. package/dist/chunk-IARLJ5MW.js.map +0 -1
  82. package/dist/chunk-LDFQUZII.cjs +0 -950
  83. package/dist/chunk-LDFQUZII.cjs.map +0 -1
  84. package/dist/chunk-NQHETFDA.js +0 -950
  85. package/dist/chunk-NQHETFDA.js.map +0 -1
  86. package/dist/chunk-SJVRUPJT.js +0 -272
  87. package/dist/chunk-SJVRUPJT.js.map +0 -1
  88. package/dist/chunk-TJJFKKH2.cjs +0 -272
  89. package/dist/chunk-TJJFKKH2.cjs.map +0 -1
  90. package/dist/chunk-VSH7ECQL.cjs +0 -1256
  91. package/dist/chunk-VSH7ECQL.cjs.map +0 -1
  92. package/dist/chunk-XS6U2ARZ.js +0 -458
  93. package/dist/chunk-XS6U2ARZ.js.map +0 -1
  94. package/dist/schemas.cjs.map +0 -1
  95. package/dist/schemas.js.map +0 -1
  96. package/dist/scoring.cjs.map +0 -1
  97. package/dist/scoring.js.map +0 -1
  98. package/dist/xapi.cjs.map +0 -1
  99. package/dist/xapi.js.map +0 -1
package/dist/ai-check.cjs CHANGED
@@ -1,500 +1,794 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
- var _chunkLDFQUZIIcjs = require('./chunk-LDFQUZII.cjs');
11
-
12
-
13
- var _chunkVSH7ECQLcjs = require('./chunk-VSH7ECQL.cjs');
14
- require('./chunk-DSDXL3JO.cjs');
15
-
16
- // src/ai-check/cases.ts
17
- var capital = {
18
- schemaVersion: "1.0",
19
- type: "multiple-choice",
20
- id: "ai-check-mc",
21
- title: "Capital cities",
22
- question: "What is the capital of Spain?",
23
- mode: "single",
24
- scoringStrategy: "all-or-nothing",
25
- options: [
26
- { id: "madrid", text: "Madrid", isCorrect: true },
27
- { id: "barcelona", text: "Barcelona", isCorrect: false },
28
- { id: "seville", text: "Seville", isCorrect: false }
29
- ]
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_ai_writing = require("./ai-writing-DCVkv4Zj.cjs");
3
+ const require_scoring = require("./scoring-B2_HEhMT.cjs");
4
+ //#region src/ai-check/cases.ts
5
+ /**
6
+ * The items the cases are built from. Small, ordinary language-course content
7
+ * — and deliberately awkward in the places a model slips:
8
+ *
9
+ * - an answer a hint can hardly avoid naming (`Madrid`);
10
+ * - an answer of one short word (`is`), which a hint may write as an ordinary
11
+ * word and must not write beside its neighbour in the passage;
12
+ * - an accented answer (`cañón`), so a hint that spells it without the accent
13
+ * is still caught;
14
+ * - a passage with more than one blank, so a hint can be about the wrong one;
15
+ * - a short essay whose mistakes a model must quote exactly as the learner
16
+ * wrote them — straight apostrophes a model likes to curl, a mistake made
17
+ * twice — so a correction of words nobody wrote is caught.
18
+ */
19
+ const capital = {
20
+ schemaVersion: "1.0",
21
+ type: "multiple-choice",
22
+ id: "ai-check-mc",
23
+ title: "Capital cities",
24
+ question: "What is the capital of Spain?",
25
+ mode: "single",
26
+ scoringStrategy: "all-or-nothing",
27
+ options: [
28
+ {
29
+ id: "madrid",
30
+ text: "Madrid",
31
+ isCorrect: true
32
+ },
33
+ {
34
+ id: "barcelona",
35
+ text: "Barcelona",
36
+ isCorrect: false
37
+ },
38
+ {
39
+ id: "seville",
40
+ text: "Seville",
41
+ isCorrect: false
42
+ }
43
+ ]
30
44
  };
31
- var introduction = {
32
- schemaVersion: "1.0",
33
- type: "fill-in-the-blanks",
34
- id: "ai-check-fib",
35
- title: "Introducing yourself",
36
- passage: "My name {{be}} Rossi and I {{live}} in Madrid.",
37
- blanks: [
38
- { id: "be", acceptedAnswers: ["is"] },
39
- { id: "live", acceptedAnswers: ["live"], hint: "Present simple, first person." }
40
- ],
41
- scoringStrategy: "partial"
45
+ const introduction = {
46
+ schemaVersion: "1.0",
47
+ type: "fill-in-the-blanks",
48
+ id: "ai-check-fib",
49
+ title: "Introducing yourself",
50
+ passage: "My name {{be}} Rossi and I {{live}} in Madrid.",
51
+ blanks: [{
52
+ id: "be",
53
+ acceptedAnswers: ["is"]
54
+ }, {
55
+ id: "live",
56
+ acceptedAnswers: ["live"],
57
+ hint: "Present simple, first person."
58
+ }],
59
+ scoringStrategy: "partial"
42
60
  };
43
- var landscape = {
44
- schemaVersion: "1.0",
45
- type: "gap-select",
46
- id: "ai-check-gap",
47
- title: "Geography",
48
- locale: "es-ES",
49
- passage: "El r\xEDo pasa por un {{g1}} muy profundo, y el agua {{g2}} fr\xEDa.",
50
- gaps: [
51
- {
52
- id: "g1",
53
- choices: [
54
- { id: "canon", text: "ca\xF1\xF3n" },
55
- { id: "campo", text: "campo" }
56
- ],
57
- correctChoiceId: "canon"
58
- },
59
- {
60
- id: "g2",
61
- choices: [
62
- { id: "esta", text: "est\xE1" },
63
- { id: "es", text: "es" }
64
- ],
65
- correctChoiceId: "esta"
66
- }
67
- ],
68
- scoringStrategy: "partial"
61
+ const landscape = {
62
+ schemaVersion: "1.0",
63
+ type: "gap-select",
64
+ id: "ai-check-gap",
65
+ title: "Geography",
66
+ locale: "es-ES",
67
+ passage: "El río pasa por un {{g1}} muy profundo, y el agua {{g2}} fría.",
68
+ gaps: [{
69
+ id: "g1",
70
+ choices: [{
71
+ id: "canon",
72
+ text: "cañón"
73
+ }, {
74
+ id: "campo",
75
+ text: "campo"
76
+ }],
77
+ correctChoiceId: "canon"
78
+ }, {
79
+ id: "g2",
80
+ choices: [{
81
+ id: "esta",
82
+ text: "está"
83
+ }, {
84
+ id: "es",
85
+ text: "es"
86
+ }],
87
+ correctChoiceId: "esta"
88
+ }],
89
+ scoringStrategy: "partial"
69
90
  };
70
- var listening = {
71
- schemaVersion: "1.0",
72
- type: "dictation",
73
- id: "ai-check-dictation",
74
- title: "Listen and type",
75
- transcript: "The train leaves at half past nine."
91
+ const listening = {
92
+ schemaVersion: "1.0",
93
+ type: "dictation",
94
+ id: "ai-check-dictation",
95
+ title: "Listen and type",
96
+ transcript: "The train leaves at half past nine."
76
97
  };
77
- var weekend = {
78
- schemaVersion: "1.0",
79
- type: "written-response",
80
- id: "ai-check-writing",
81
- title: "Your weekend",
82
- prompt: "Write a few sentences about what you did last weekend.",
83
- minWords: 15,
84
- maxWords: 120,
85
- languageTarget: "en-A2",
86
- rubric: {
87
- criteria: [
88
- { name: "Grammar", description: "Past tenses, articles, agreement", weight: 2 },
89
- { name: "Vocabulary", description: "Everyday words, used correctly", weight: 1 },
90
- { name: "Task", description: "Says what the learner did, in order", weight: 1 }
91
- ]
92
- }
98
+ const weekend = {
99
+ schemaVersion: "1.0",
100
+ type: "written-response",
101
+ id: "ai-check-writing",
102
+ title: "Your weekend",
103
+ prompt: "Write a few sentences about what you did last weekend.",
104
+ minWords: 15,
105
+ maxWords: 120,
106
+ languageTarget: "en-A2",
107
+ rubric: { criteria: [
108
+ {
109
+ name: "Grammar",
110
+ description: "Past tenses, articles, agreement",
111
+ weight: 2
112
+ },
113
+ {
114
+ name: "Vocabulary",
115
+ description: "Everyday words, used correctly",
116
+ weight: 1
117
+ },
118
+ {
119
+ name: "Task",
120
+ description: "Says what the learner did, in order",
121
+ weight: 1
122
+ }
123
+ ] }
93
124
  };
94
- var weather = {
95
- schemaVersion: "1.0",
96
- type: "read-aloud",
97
- id: "ai-check-reading",
98
- title: "Read the forecast aloud",
99
- referenceText: "The weather is warm in the north.",
100
- locale: "en-US",
101
- recording: { maxSeconds: 20 },
102
- scoring: {
103
- dimensions: [
104
- { name: "accuracy", weight: 2 },
105
- { name: "fluency", weight: 1 }
106
- ]
107
- }
125
+ const weather = {
126
+ schemaVersion: "1.0",
127
+ type: "read-aloud",
128
+ id: "ai-check-reading",
129
+ title: "Read the forecast aloud",
130
+ referenceText: "The weather is warm in the north.",
131
+ locale: "en-US",
132
+ recording: { maxSeconds: 20 },
133
+ scoring: { dimensions: [{
134
+ name: "accuracy",
135
+ weight: 2
136
+ }, {
137
+ name: "fluency",
138
+ weight: 1
139
+ }] }
108
140
  };
109
- var said = (text, over = {}) => ({
110
- text,
111
- error: "none",
112
- accuracy: 92,
113
- ...over
141
+ /** A word of the forecast as an engine reports it. */
142
+ const said = (text, over = {}) => ({
143
+ text,
144
+ error: "none",
145
+ accuracy: 92,
146
+ ...over
114
147
  });
148
+ /**
149
+ * An engine's assessment of the forecast. With `slips`, "weather" is
150
+ * mispronounced — its "th" heard as /d/ or /z/ — and "north" is left out: the
151
+ * marks a model must coach, and no others.
152
+ */
115
153
  function forecast(slips) {
116
- return {
117
- assessmentVersion: "1.0",
118
- status: "assessed",
119
- task: "scripted",
120
- locale: "en-US",
121
- referenceText: weather.referenceText,
122
- recordingKey: "ai-check-take",
123
- assessor: { kind: "auto", id: "pronunciation-engine" },
124
- scale: 100,
125
- scores: slips ? { accuracy: 68, fluency: 81, completeness: 86 } : { accuracy: 94, fluency: 90 },
126
- miscue: "assessor",
127
- phonemeAlphabet: "ipa",
128
- words: [
129
- said("The"),
130
- slips ? said("weather", {
131
- accuracy: 38,
132
- error: "mispronunciation",
133
- phonemes: [
134
- { symbol: "w", accuracy: 90 },
135
- { symbol: "\u025B", accuracy: 85 },
136
- {
137
- symbol: "\xF0",
138
- accuracy: 12,
139
- heardAs: [
140
- { symbol: "d", score: 64 },
141
- { symbol: "z", score: 21 },
142
- { symbol: "\xF0", score: 12 }
143
- ]
144
- },
145
- { symbol: "\u025A", accuracy: 80 }
146
- ]
147
- }) : said("weather"),
148
- said("is"),
149
- said("warm"),
150
- said("in"),
151
- said("the"),
152
- slips ? { text: "north", error: "omission" } : said("north")
153
- ]
154
- };
154
+ return {
155
+ assessmentVersion: "1.0",
156
+ status: "assessed",
157
+ task: "scripted",
158
+ locale: "en-US",
159
+ referenceText: weather.referenceText,
160
+ recordingKey: "ai-check-take",
161
+ assessor: {
162
+ kind: "auto",
163
+ id: "pronunciation-engine"
164
+ },
165
+ scale: 100,
166
+ scores: slips ? {
167
+ accuracy: 68,
168
+ fluency: 81,
169
+ completeness: 86
170
+ } : {
171
+ accuracy: 94,
172
+ fluency: 90
173
+ },
174
+ miscue: "assessor",
175
+ phonemeAlphabet: "ipa",
176
+ words: [
177
+ said("The"),
178
+ slips ? said("weather", {
179
+ accuracy: 38,
180
+ error: "mispronunciation",
181
+ phonemes: [
182
+ {
183
+ symbol: "w",
184
+ accuracy: 90
185
+ },
186
+ {
187
+ symbol: "ɛ",
188
+ accuracy: 85
189
+ },
190
+ {
191
+ symbol: "ð",
192
+ accuracy: 12,
193
+ heardAs: [
194
+ {
195
+ symbol: "d",
196
+ score: 64
197
+ },
198
+ {
199
+ symbol: "z",
200
+ score: 21
201
+ },
202
+ {
203
+ symbol: "ð",
204
+ score: 12
205
+ }
206
+ ]
207
+ },
208
+ {
209
+ symbol: "ɚ",
210
+ accuracy: 80
211
+ }
212
+ ]
213
+ }) : said("weather"),
214
+ said("is"),
215
+ said("warm"),
216
+ said("in"),
217
+ said("the"),
218
+ slips ? {
219
+ text: "north",
220
+ error: "omission"
221
+ } : said("north")
222
+ ]
223
+ };
155
224
  }
156
- var chose = (ids) => ({
157
- type: "multiple-choice",
158
- selectedOptionIds: ids
225
+ const chose = (ids) => ({
226
+ type: "multiple-choice",
227
+ selectedOptionIds: ids
228
+ });
229
+ const typed = (answers) => ({
230
+ type: "fill-in-the-blanks",
231
+ answers
159
232
  });
160
- var typed = (answers) => ({
161
- type: "fill-in-the-blanks",
162
- answers
233
+ const picked = (choices) => ({
234
+ type: "gap-select",
235
+ selections: choices
163
236
  });
164
- var picked = (choices) => ({
165
- type: "gap-select",
166
- selections: choices
237
+ const wrote = (text) => ({
238
+ type: "dictation",
239
+ text
167
240
  });
168
- var wrote = (text) => ({ type: "dictation", text });
169
- var drafted = (text) => ({
170
- type: "written-response",
171
- text,
172
- wordCount: 0
241
+ const drafted = (text) => ({
242
+ type: "written-response",
243
+ text,
244
+ wordCount: 0
173
245
  });
246
+ /**
247
+ * Items an author asks a model to review. Each is valid, and each but the last
248
+ * has a flaw a rule cannot see: a distractor that is botanically a right
249
+ * answer, and a task far above the level it is set for.
250
+ */
251
+ const fruit = {
252
+ schemaVersion: "1.0",
253
+ type: "multiple-choice",
254
+ id: "ai-check-fruit",
255
+ title: "Food words",
256
+ question: "Which of these is a fruit?",
257
+ mode: "single",
258
+ scoringStrategy: "all-or-nothing",
259
+ options: [
260
+ {
261
+ id: "f1",
262
+ text: "Apple",
263
+ isCorrect: true
264
+ },
265
+ {
266
+ id: "f2",
267
+ text: "Carrot",
268
+ isCorrect: false
269
+ },
270
+ {
271
+ id: "f3",
272
+ text: "Tomato",
273
+ isCorrect: false
274
+ }
275
+ ]
276
+ };
277
+ const tooHard = {
278
+ schemaVersion: "1.0",
279
+ type: "written-response",
280
+ id: "ai-check-too-hard",
281
+ title: "Cities",
282
+ prompt: "Discuss the socioeconomic ramifications of rapid urbanisation in developing economies.",
283
+ minWords: 40,
284
+ maxWords: 80
285
+ };
286
+ const cleanCloze = {
287
+ schemaVersion: "1.0",
288
+ type: "fill-in-the-blanks",
289
+ id: "ai-check-clean-cloze",
290
+ title: "Daily routine",
291
+ passage: "Every morning I {{1}} up at seven and {{2}} a shower.",
292
+ blanks: [{
293
+ id: "1",
294
+ acceptedAnswers: ["get", "wake"]
295
+ }, {
296
+ id: "2",
297
+ acceptedAnswers: ["take", "have"]
298
+ }],
299
+ scoringStrategy: "partial"
300
+ };
301
+ function critiqueCase(id, about, type, draft, extra = {}) {
302
+ const request = require_ai_writing.aiCritiqueRequest({
303
+ type,
304
+ draft,
305
+ ...extra
306
+ });
307
+ if (request === null) throw new Error(`ai-check: no critique request for "${id}"`);
308
+ return {
309
+ id,
310
+ feature: "item-critique",
311
+ about,
312
+ request
313
+ };
314
+ }
315
+ /** A short reading to draft from: plain, and with facts a question can be asked about. */
316
+ const MARKET = "Maria lives in Seville, in the south of Spain. Every Saturday she walks to the market with her grandmother. They buy oranges, fresh bread and a little cheese. On the way home they stop at a café, where Maria drinks hot chocolate and her grandmother reads the newspaper.";
317
+ /** The same reading as a video's captions, with times in seconds. */
318
+ const MARKET_CAPTIONS = [
319
+ {
320
+ start: 0,
321
+ end: 4.2,
322
+ text: "Maria lives in Seville, in the south of Spain."
323
+ },
324
+ {
325
+ start: 4.2,
326
+ end: 8.9,
327
+ text: "Every Saturday she walks to the market with her grandmother."
328
+ },
329
+ {
330
+ start: 8.9,
331
+ end: 12.6,
332
+ text: "They buy oranges, fresh bread and a little cheese."
333
+ },
334
+ {
335
+ start: 12.6,
336
+ end: 18.1,
337
+ text: "On the way home they stop at a café."
338
+ }
339
+ ];
340
+ function draftsCase(id, about, types, source, extra = {}) {
341
+ const request = require_ai_writing.aiDraftsRequest({
342
+ types,
343
+ source,
344
+ ...extra
345
+ });
346
+ if (request === null) throw new Error(`ai-check: no drafts request for "${id}"`);
347
+ return {
348
+ id,
349
+ feature: "draft-generation",
350
+ about,
351
+ request
352
+ };
353
+ }
354
+ /**
355
+ * What a host sets on every read-aloud it drafts: the SDK has no default for a
356
+ * recording's length or the dimensions a reading is graded on.
357
+ */
358
+ const READ_ALOUD_SETTINGS = {
359
+ locale: "en-US",
360
+ recording: { maxSeconds: 20 },
361
+ scoring: { dimensions: [{
362
+ name: "accuracy",
363
+ weight: 1
364
+ }] }
365
+ };
366
+ /** Builds a case, failing loudly rather than shipping a case with no request. */
174
367
  function explanationCase(id, about, data, response) {
175
- const request = _chunkLDFQUZIIcjs.aiExplanationRequest.call(void 0, { data, response });
176
- if (request === null) {
177
- throw new Error(`ai-check: no explanation request for "${id}"`);
178
- }
179
- return { id, feature: "explanation", about, request };
368
+ const request = require_ai_writing.aiExplanationRequest({
369
+ data,
370
+ response
371
+ });
372
+ if (request === null) throw new Error(`ai-check: no explanation request for "${id}"`);
373
+ return {
374
+ id,
375
+ feature: "explanation",
376
+ about,
377
+ request
378
+ };
180
379
  }
181
380
  function hintCase(id, about, data, response, previousHints = []) {
182
- const request = _chunkLDFQUZIIcjs.aiHintRequest.call(void 0, { data, response, previousHints });
183
- if (request === null) {
184
- throw new Error(`ai-check: no hint request for "${id}"`);
185
- }
186
- return { id, feature: "hint", about, request };
381
+ const request = require_ai_writing.aiHintRequest({
382
+ data,
383
+ response,
384
+ previousHints
385
+ });
386
+ if (request === null) throw new Error(`ai-check: no hint request for "${id}"`);
387
+ return {
388
+ id,
389
+ feature: "hint",
390
+ about,
391
+ request
392
+ };
187
393
  }
188
394
  function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
189
- const request = _chunkLDFQUZIIcjs.aiWritingFeedbackRequest.call(void 0, {
190
- data: weekend,
191
- response: drafted(text),
192
- previousFeedback,
193
- ...learnerLocale !== void 0 ? { learnerLocale } : {}
194
- });
195
- if (request === null) {
196
- throw new Error(`ai-check: no writing feedback request for "${id}"`);
197
- }
198
- return { id, feature: "writing-feedback", about, request };
395
+ const request = require_ai_writing.aiWritingFeedbackRequest({
396
+ data: weekend,
397
+ response: drafted(text),
398
+ previousFeedback,
399
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
400
+ });
401
+ if (request === null) throw new Error(`ai-check: no writing feedback request for "${id}"`);
402
+ return {
403
+ id,
404
+ feature: "writing-feedback",
405
+ about,
406
+ request
407
+ };
199
408
  }
200
409
  function coachingCase(id, about, marks, learnerLocale) {
201
- const assessment = _nullishCoalesce(marks.assessment, () => ( forecast(true)));
202
- const grade = _chunkVSH7ECQLcjs.gradeReadAloud.call(void 0,
203
- weather,
204
- { type: "read-aloud", recording: { key: "ai-check-take", mimeType: "audio/wav" } },
205
- assessment,
206
- {
207
- measured: { durationMs: 4e3, voicedMs: 3e3 },
208
- plausibility: { maxWordsPerSecond: 6, minVoicedMs: 500 }
209
- }
210
- );
211
- if ("unscorable" in grade) {
212
- throw new Error(`ai-check: the reading for "${id}" did not grade: ${grade.reason}`);
213
- }
214
- const request = _chunkLDFQUZIIcjs.aiCoachingRequest.call(void 0, {
215
- data: weather,
216
- ...marks.fromGradeOnly === true ? {} : { assessment },
217
- grade,
218
- ...learnerLocale !== void 0 ? { learnerLocale } : {}
219
- });
220
- if (request === null) {
221
- throw new Error(`ai-check: no coaching request for "${id}"`);
222
- }
223
- return { id, feature: "pronunciation-coaching", about, request };
410
+ const assessment = marks.assessment ?? forecast(true);
411
+ const grade = require_scoring.gradeReadAloud(weather, {
412
+ type: "read-aloud",
413
+ recording: {
414
+ key: "ai-check-take",
415
+ mimeType: "audio/wav"
416
+ }
417
+ }, assessment, {
418
+ measured: {
419
+ durationMs: 4e3,
420
+ voicedMs: 3e3
421
+ },
422
+ plausibility: {
423
+ maxWordsPerSecond: 6,
424
+ minVoicedMs: 500
425
+ }
426
+ });
427
+ if ("unscorable" in grade) throw new Error(`ai-check: the reading for "${id}" did not grade: ${grade.reason}`);
428
+ const request = require_ai_writing.aiCoachingRequest({
429
+ data: weather,
430
+ ...marks.fromGradeOnly === true ? {} : { assessment },
431
+ grade,
432
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
433
+ });
434
+ if (request === null) throw new Error(`ai-check: no coaching request for "${id}"`);
435
+ return {
436
+ id,
437
+ feature: "pronunciation-coaching",
438
+ about,
439
+ request
440
+ };
224
441
  }
442
+ /**
443
+ * The cases {@link runAiCheck} runs: every type the SDK explains, answered
444
+ * right, wrong and — where a type can be — partly right; every type it hints
445
+ * for, before an answer and after a wrong one; and drafts of an essay, with
446
+ * mistakes, without, and revised.
447
+ *
448
+ * They are ordinary calls. What makes them a test is that the SDK knows the
449
+ * answer to each, so it can check what your model writes about it.
450
+ *
451
+ * A fresh array each call, so a caller may filter, slice or add to it without
452
+ * changing what the next caller gets.
453
+ */
225
454
  function aiCheckCases() {
226
- return [
227
- explanationCase("mc-correct", "Chose the right city", capital, chose(["madrid"])),
228
- explanationCase("mc-wrong", "Chose Barcelona", capital, chose(["barcelona"])),
229
- explanationCase("mc-blank", "Chose nothing and submitted", capital, chose([])),
230
- explanationCase(
231
- "fib-correct",
232
- "Filled both blanks correctly",
233
- introduction,
234
- typed({ be: "is", live: "live" })
235
- ),
236
- explanationCase(
237
- "fib-partly",
238
- "One blank right, one wrong",
239
- introduction,
240
- typed({ be: "is", live: "lives" })
241
- ),
242
- explanationCase(
243
- "fib-wrong",
244
- "Both blanks wrong",
245
- introduction,
246
- typed({ be: "are", live: "living" })
247
- ),
248
- explanationCase(
249
- "gap-partly",
250
- "One gap right, one wrong, in Spanish",
251
- landscape,
252
- picked({ g1: "canon", g2: "es" })
253
- ),
254
- explanationCase(
255
- "gap-wrong",
256
- "Both gaps wrong, in Spanish",
257
- landscape,
258
- picked({ g1: "campo", g2: "es" })
259
- ),
260
- explanationCase(
261
- "dictation-near",
262
- "Typed the sentence with two words wrong",
263
- listening,
264
- wrote("The train leaves at half past five")
265
- ),
266
- explanationCase(
267
- "dictation-correct",
268
- "Typed the sentence correctly",
269
- listening,
270
- wrote("The train leaves at half past nine.")
271
- ),
272
- hintCase("mc-hint-1", "Has not answered yet", capital, chose([])),
273
- hintCase("mc-hint-2", "Asked again after choosing Seville", capital, chose(["seville"]), [
274
- "Think about where the government sits."
275
- ]),
276
- hintCase("fib-hint-1", "Both blanks empty", introduction, typed({})),
277
- hintCase(
278
- "fib-hint-2",
279
- "Asked again with one blank wrong",
280
- introduction,
281
- typed({ be: "are", live: "" }),
282
- ["Look again at the first blank."]
283
- ),
284
- hintCase("gap-hint-1", "Neither gap chosen, in Spanish", landscape, picked({})),
285
- hintCase(
286
- "gap-hint-2",
287
- "Asked again with the first gap wrong",
288
- landscape,
289
- picked({ g1: "campo" }),
290
- ["Piensa en la forma del terreno."]
291
- ),
292
- writingCase(
293
- "writing-mistakes",
294
- "A first draft with tense, article and agreement mistakes",
295
- "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."
296
- ),
297
- writingCase(
298
- "writing-clean",
299
- "A first draft with no mistakes to correct",
300
- "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."
301
- ),
302
- writingCase(
303
- "writing-revised",
304
- "A second draft that fixed some mistakes",
305
- "Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.",
306
- ['Check your past tenses: "go" and "buyed" should be past forms.']
307
- ),
308
- writingCase(
309
- "writing-feedback-in-spanish",
310
- "A first draft, with feedback asked for in Spanish",
311
- "Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.",
312
- [],
313
- "es"
314
- ),
315
- coachingCase("coaching-slips", 'A reading with a mispronounced "th" and a word left out', {
316
- assessment: forecast(true)
317
- }),
318
- coachingCase("coaching-clean", "A reading the engine marked correct throughout", {
319
- assessment: forecast(false)
320
- }),
321
- coachingCase(
322
- "coaching-from-grade",
323
- "The same slips, from a stored grade: marks without sounds",
324
- { fromGradeOnly: true }
325
- ),
326
- coachingCase(
327
- "coaching-in-spanish",
328
- "The same slips, with coaching asked for in Spanish",
329
- { assessment: forecast(true) },
330
- "es"
331
- )
332
- ];
455
+ return [
456
+ explanationCase("mc-correct", "Chose the right city", capital, chose(["madrid"])),
457
+ explanationCase("mc-wrong", "Chose Barcelona", capital, chose(["barcelona"])),
458
+ explanationCase("mc-blank", "Chose nothing and submitted", capital, chose([])),
459
+ explanationCase("fib-correct", "Filled both blanks correctly", introduction, typed({
460
+ be: "is",
461
+ live: "live"
462
+ })),
463
+ explanationCase("fib-partly", "One blank right, one wrong", introduction, typed({
464
+ be: "is",
465
+ live: "lives"
466
+ })),
467
+ explanationCase("fib-wrong", "Both blanks wrong", introduction, typed({
468
+ be: "are",
469
+ live: "living"
470
+ })),
471
+ explanationCase("gap-partly", "One gap right, one wrong, in Spanish", landscape, picked({
472
+ g1: "canon",
473
+ g2: "es"
474
+ })),
475
+ explanationCase("gap-wrong", "Both gaps wrong, in Spanish", landscape, picked({
476
+ g1: "campo",
477
+ g2: "es"
478
+ })),
479
+ explanationCase("dictation-near", "Typed the sentence with two words wrong", listening, wrote("The train leaves at half past five")),
480
+ explanationCase("dictation-correct", "Typed the sentence correctly", listening, wrote("The train leaves at half past nine.")),
481
+ hintCase("mc-hint-1", "Has not answered yet", capital, chose([])),
482
+ hintCase("mc-hint-2", "Asked again after choosing Seville", capital, chose(["seville"]), ["Think about where the government sits."]),
483
+ hintCase("fib-hint-1", "Both blanks empty", introduction, typed({})),
484
+ hintCase("fib-hint-2", "Asked again with one blank wrong", introduction, typed({
485
+ be: "are",
486
+ live: ""
487
+ }), ["Look again at the first blank."]),
488
+ hintCase("gap-hint-1", "Neither gap chosen, in Spanish", landscape, picked({})),
489
+ hintCase("gap-hint-2", "Asked again with the first gap wrong", landscape, picked({ g1: "campo" }), ["Piensa en la forma del terreno."]),
490
+ writingCase("writing-mistakes", "A first draft with tense, article and agreement mistakes", "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."),
491
+ writingCase("writing-clean", "A first draft with no mistakes to correct", "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."),
492
+ writingCase("writing-revised", "A second draft that fixed some mistakes", "Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.", ["Check your past tenses: \"go\" and \"buyed\" should be past forms."]),
493
+ writingCase("writing-feedback-in-spanish", "A first draft, with feedback asked for in Spanish", "Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.", [], "es"),
494
+ coachingCase("coaching-slips", "A reading with a mispronounced \"th\" and a word left out", { assessment: forecast(true) }),
495
+ coachingCase("coaching-clean", "A reading the engine marked correct throughout", { assessment: forecast(false) }),
496
+ coachingCase("coaching-from-grade", "The same slips, from a stored grade: marks without sounds", { fromGradeOnly: true }),
497
+ coachingCase("coaching-in-spanish", "The same slips, with coaching asked for in Spanish", { assessment: forecast(true) }, "es"),
498
+ critiqueCase("critique-second-answer", "A question whose distractor \"Tomato\" is botanically a fruit", "multiple-choice", fruit, { level: "A1" }),
499
+ critiqueCase("critique-level", "A writing task far above the A1 level it is set for", "written-response", tooHard, { level: "A1" }),
500
+ critiqueCase("critique-clean", "A cloze with nothing wrong in it", "fill-in-the-blanks", cleanCloze, { level: "A2" }),
501
+ critiqueCase("critique-in-spanish", "The fruit question, reviewed in Spanish for its author", "multiple-choice", fruit, {
502
+ level: "A1",
503
+ authorLocale: "es"
504
+ }),
505
+ draftsCase("drafts-questions", "Four multiple-choice questions on a short reading", ["multiple-choice"], {
506
+ kind: "passage",
507
+ text: MARKET
508
+ }, {
509
+ count: 4,
510
+ level: "A2",
511
+ locale: "en"
512
+ }),
513
+ draftsCase("drafts-cloze", "A cloze on the same reading, from its transcript", ["fill-in-the-blanks"], {
514
+ kind: "transcript",
515
+ text: MARKET
516
+ }, {
517
+ count: 1,
518
+ level: "A2",
519
+ locale: "en",
520
+ instructions: "Blank the verbs."
521
+ }),
522
+ draftsCase("drafts-gaps", "A choose-the-word item on the same reading", ["gap-select"], {
523
+ kind: "passage",
524
+ text: MARKET
525
+ }, {
526
+ count: 1,
527
+ level: "A2",
528
+ locale: "en"
529
+ }),
530
+ draftsCase("drafts-writing", "A writing task that follows on from the reading", ["written-response"], {
531
+ kind: "passage",
532
+ text: MARKET
533
+ }, {
534
+ count: 1,
535
+ level: "A2",
536
+ locale: "en"
537
+ }),
538
+ draftsCase("drafts-video", "A video quiz from timed captions: multiple choice, then dictation, then read-aloud, as many as the video is worth", [
539
+ "multiple-choice",
540
+ "dictation",
541
+ "read-aloud"
542
+ ], {
543
+ kind: "captions",
544
+ cues: MARKET_CAPTIONS
545
+ }, {
546
+ level: "A2",
547
+ locale: "en",
548
+ settings: { "read-aloud": READ_ALOUD_SETTINGS }
549
+ }),
550
+ draftsCase("drafts-script", "The same video from its script alone, with no times, and what the author asked for", [
551
+ "multiple-choice",
552
+ "dictation",
553
+ "read-aloud"
554
+ ], {
555
+ kind: "transcript",
556
+ text: MARKET
557
+ }, {
558
+ level: "A2",
559
+ locale: "en",
560
+ instructions: "Focus on what Maria and her grandmother buy.",
561
+ settings: { "read-aloud": READ_ALOUD_SETTINGS }
562
+ })
563
+ ];
333
564
  }
334
-
335
- // src/ai-check/index.ts
336
- var REFUSALS = [
337
- "malformed",
338
- "empty",
339
- "too-long",
340
- "contradicts-grade",
341
- "reveals-answer",
342
- "misquotes-answer",
343
- "contradicts-marks"
565
+ //#endregion
566
+ //#region src/ai-check/index.ts
567
+ /**
568
+ * A test kit for the prompts behind your AI ports.
569
+ *
570
+ * The SDK cannot test your model, and will not try. What it can do is make the
571
+ * calls a learner's questions would make — on items whose answers it knows —
572
+ * and run the same checks it runs before a learner sees anything. What comes
573
+ * back is a count of what would have been shown and what would have been
574
+ * refused, and why.
575
+ *
576
+ * Run it where you change a prompt or a model, in your own CI, with your own
577
+ * key. It is a separate entry point (`@intellectif/lk-core/ai-check`) because
578
+ * it is for your test run, not for a learner's browser: nothing here is
579
+ * imported by anything a page renders.
580
+ *
581
+ * ```ts
582
+ * import { aiCheckCases, formatAiCheckReport, runAiCheck } from '@intellectif/lk-core/ai-check';
583
+ *
584
+ * const report = await runAiCheck({
585
+ * explain: (request) => callMyModel(EXPLAIN_PROMPT, request),
586
+ * hint: (request) => callMyModel(HINT_PROMPT, request),
587
+ * writingFeedback: (request) => callMyModel(FEEDBACK_PROMPT, request),
588
+ * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),
589
+ * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),
590
+ * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),
591
+ * });
592
+ * console.log(formatAiCheckReport(report));
593
+ * if (report.refused > 0 || report.errors > 0) {
594
+ * process.exitCode = 1;
595
+ * }
596
+ * ```
597
+ *
598
+ * A refusal is a real failure, not a warning: it is an answer a learner would
599
+ * have asked for and not received. `reveals-answer` on a hint case is the one
600
+ * to take most seriously — that prompt gives answers away. Next come
601
+ * `misquotes-answer` on a writing case — that prompt corrects words the learner
602
+ * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches
603
+ * words the engine heard as right, or sounds it never reported.
604
+ */
605
+ const REFUSALS = [
606
+ "malformed",
607
+ "empty",
608
+ "too-long",
609
+ "contradicts-grade",
610
+ "reveals-answer",
611
+ "misquotes-answer",
612
+ "contradicts-marks",
613
+ "contradicts-item"
344
614
  ];
615
+ /** The port a case is for. */
345
616
  function portFor(ports, one) {
346
- switch (one.request.feature) {
347
- case "explanation":
348
- return ports.explain;
349
- case "hint":
350
- return ports.hint;
351
- case "pronunciation-coaching":
352
- return ports.pronunciationCoaching;
353
- default:
354
- return ports.writingFeedback;
355
- }
617
+ switch (one.request.feature) {
618
+ case "explanation": return ports.explain;
619
+ case "hint": return ports.hint;
620
+ case "pronunciation-coaching": return ports.pronunciationCoaching;
621
+ case "item-critique": return ports.critique;
622
+ case "draft-generation": return ports.drafts;
623
+ default: return ports.writingFeedback;
624
+ }
356
625
  }
357
- var now = () => Date.now();
626
+ /** Milliseconds, from whichever clock this runtime has. */
627
+ const now = () => Date.now();
628
+ /** Runs one case: calls the port, times it, and checks what came back. */
358
629
  async function runCase(ports, one) {
359
- const started = now();
360
- const port = portFor(ports, one);
361
- if (port === void 0) {
362
- throw new Error(`ai-check: no port for "${one.id}"`);
363
- }
364
- let raw;
365
- try {
366
- raw = await port(one.request);
367
- } catch (error) {
368
- return {
369
- case: one,
370
- ok: false,
371
- error: error instanceof Error ? error.message : String(error),
372
- ms: now() - started
373
- };
374
- }
375
- const ms = now() - started;
376
- if (one.request.feature === "pronunciation-coaching") {
377
- const checked2 = _chunkLDFQUZIIcjs.checkAiCoaching.call(void 0, raw, one.request);
378
- if (checked2.ok) {
379
- const { text, provenance, usage } = checked2.coaching;
380
- return {
381
- case: one,
382
- ok: true,
383
- result: {
384
- text,
385
- ...provenance !== void 0 ? { provenance } : {},
386
- ...usage !== void 0 ? { usage } : {}
387
- },
388
- coaching: checked2.coaching,
389
- ms
390
- };
391
- }
392
- return refused(one, raw, checked2.refusal, ms);
393
- }
394
- if (one.request.feature === "writing-feedback") {
395
- const checked2 = _chunkLDFQUZIIcjs.checkAiWritingFeedback.call(void 0, raw, one.request);
396
- if (checked2.ok) {
397
- const { text, provenance, usage } = checked2.feedback;
398
- return {
399
- case: one,
400
- ok: true,
401
- result: {
402
- text,
403
- ...provenance !== void 0 ? { provenance } : {},
404
- ...usage !== void 0 ? { usage } : {}
405
- },
406
- feedback: checked2.feedback,
407
- ms
408
- };
409
- }
410
- return refused(one, raw, checked2.refusal, ms);
411
- }
412
- const checked = one.request.feature === "explanation" ? _chunkLDFQUZIIcjs.checkAiExplanation.call(void 0, raw, one.request) : _chunkLDFQUZIIcjs.checkAiHint.call(void 0, raw, one.request);
413
- if (checked.ok) {
414
- return { case: one, ok: true, result: checked.result, ms };
415
- }
416
- return refused(one, raw, checked.refusal, ms);
630
+ const started = now();
631
+ const port = portFor(ports, one);
632
+ if (port === void 0) throw new Error(`ai-check: no port for "${one.id}"`);
633
+ let raw;
634
+ try {
635
+ raw = await port(one.request);
636
+ } catch (error) {
637
+ return {
638
+ case: one,
639
+ ok: false,
640
+ error: error instanceof Error ? error.message : String(error),
641
+ ms: now() - started
642
+ };
643
+ }
644
+ const ms = now() - started;
645
+ if (one.request.feature === "draft-generation") {
646
+ let next = 0;
647
+ const checked = require_ai_writing.checkAiDrafts(raw, one.request, { newId: () => {
648
+ next += 1;
649
+ return `ai-check-${next}`;
650
+ } });
651
+ return checked.ok ? {
652
+ case: one,
653
+ ok: true,
654
+ drafts: checked.drafts,
655
+ ms
656
+ } : refused(one, raw, checked.refusal, ms);
657
+ }
658
+ if (one.request.feature === "item-critique") {
659
+ const checked = require_ai_writing.checkAiCritique(raw, one.request);
660
+ return checked.ok ? {
661
+ case: one,
662
+ ok: true,
663
+ critique: checked.critique,
664
+ ms
665
+ } : refused(one, raw, checked.refusal, ms);
666
+ }
667
+ if (one.request.feature === "pronunciation-coaching") {
668
+ const checked = require_ai_writing.checkAiCoaching(raw, one.request);
669
+ if (checked.ok) {
670
+ const { text, provenance, usage } = checked.coaching;
671
+ return {
672
+ case: one,
673
+ ok: true,
674
+ result: {
675
+ text,
676
+ ...provenance !== void 0 ? { provenance } : {},
677
+ ...usage !== void 0 ? { usage } : {}
678
+ },
679
+ coaching: checked.coaching,
680
+ ms
681
+ };
682
+ }
683
+ return refused(one, raw, checked.refusal, ms);
684
+ }
685
+ if (one.request.feature === "writing-feedback") {
686
+ const checked = require_ai_writing.checkAiWritingFeedback(raw, one.request);
687
+ if (checked.ok) {
688
+ const { text, provenance, usage } = checked.feedback;
689
+ return {
690
+ case: one,
691
+ ok: true,
692
+ result: {
693
+ text,
694
+ ...provenance !== void 0 ? { provenance } : {},
695
+ ...usage !== void 0 ? { usage } : {}
696
+ },
697
+ feedback: checked.feedback,
698
+ ms
699
+ };
700
+ }
701
+ return refused(one, raw, checked.refusal, ms);
702
+ }
703
+ const checked = one.request.feature === "explanation" ? require_ai_writing.checkAiExplanation(raw, one.request) : require_ai_writing.checkAiHint(raw, one.request);
704
+ if (checked.ok) return {
705
+ case: one,
706
+ ok: true,
707
+ result: checked.result,
708
+ ms
709
+ };
710
+ return refused(one, raw, checked.refusal, ms);
417
711
  }
712
+ /** A case whose answer was refused, with the text that came back when there was one to read. */
418
713
  function refused(one, raw, refusal, ms) {
419
- const text = typeof _optionalChain([raw, 'optionalAccess', _ => _.text]) === "string" ? raw : void 0;
420
- return {
421
- case: one,
422
- ok: false,
423
- refusal,
424
- ...text !== void 0 ? { result: text } : {},
425
- ms
426
- };
714
+ const text = typeof raw?.text === "string" ? raw : void 0;
715
+ return {
716
+ case: one,
717
+ ok: false,
718
+ refusal,
719
+ ...text !== void 0 ? { result: text } : {},
720
+ ms
721
+ };
427
722
  }
723
+ /**
724
+ * Calls your ports on every case and reports what a learner would have seen.
725
+ *
726
+ * It never throws for a failing case: a port that rejects is an `error` in the
727
+ * report, so one bad call cannot hide the rest.
728
+ */
428
729
  async function runAiCheck(ports, options = {}) {
429
- const all = _nullishCoalesce(options.cases, () => ( aiCheckCases()));
430
- const asked = Math.trunc(_nullishCoalesce(options.concurrency, () => ( 1)));
431
- const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;
432
- const runnable = all.filter((one) => portFor(ports, one) !== void 0);
433
- const results = new Array(runnable.length);
434
- let next = 0;
435
- const lane = async () => {
436
- while (next < runnable.length) {
437
- const mine = next;
438
- next += 1;
439
- const one = runnable[mine];
440
- if (one === void 0) {
441
- return;
442
- }
443
- results[mine] = await runCase(ports, one);
444
- }
445
- };
446
- await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));
447
- const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0]));
448
- let shown = 0;
449
- let refused2 = 0;
450
- let errors = 0;
451
- for (const result of results) {
452
- if (result.ok) {
453
- shown += 1;
454
- } else if (result.refusal !== void 0) {
455
- refused2 += 1;
456
- byRefusal[result.refusal] += 1;
457
- } else {
458
- errors += 1;
459
- }
460
- }
461
- return {
462
- total: results.length,
463
- shown,
464
- refused: refused2,
465
- errors,
466
- skipped: all.length - runnable.length,
467
- byRefusal,
468
- results
469
- };
730
+ const all = options.cases ?? aiCheckCases();
731
+ const asked = Math.trunc(options.concurrency ?? 1);
732
+ const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;
733
+ const runnable = all.filter((one) => portFor(ports, one) !== void 0);
734
+ const results = new Array(runnable.length);
735
+ let next = 0;
736
+ const lane = async () => {
737
+ while (next < runnable.length) {
738
+ const mine = next;
739
+ next += 1;
740
+ const one = runnable[mine];
741
+ if (one === void 0) return;
742
+ results[mine] = await runCase(ports, one);
743
+ }
744
+ };
745
+ await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));
746
+ const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0]));
747
+ let shown = 0;
748
+ let refused = 0;
749
+ let errors = 0;
750
+ for (const result of results) if (result.ok) shown += 1;
751
+ else if (result.refusal !== void 0) {
752
+ refused += 1;
753
+ byRefusal[result.refusal] += 1;
754
+ } else errors += 1;
755
+ return {
756
+ total: results.length,
757
+ shown,
758
+ refused,
759
+ errors,
760
+ skipped: all.length - runnable.length,
761
+ byRefusal,
762
+ results
763
+ };
470
764
  }
765
+ /**
766
+ * The report as a few lines for a CI log: the counts, then every case that
767
+ * failed, with what came back.
768
+ *
769
+ * The refused text is printed — this is your own test run, not a learner's
770
+ * screen, and a hint that gives the answer away is only fixable if you can
771
+ * read it.
772
+ */
471
773
  function formatAiCheckReport(report) {
472
- const lines = [];
473
- const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);
474
- lines.push(
475
- `ai-check: ${report.shown}/${report.total} shown, ${report.refused} refused, ${report.errors} failed${report.skipped > 0 ? `, ${report.skipped} skipped (no port)` : ""} \u2014 slowest ${slowest} ms`
476
- );
477
- for (const reason of REFUSALS) {
478
- const count = report.byRefusal[reason];
479
- if (count > 0) {
480
- lines.push(` ${reason}: ${count}`);
481
- }
482
- }
483
- for (const result of report.results) {
484
- if (result.ok) {
485
- continue;
486
- }
487
- const why = _nullishCoalesce(result.refusal, () => ( `port failed (${_nullishCoalesce(result.error, () => ( "no message"))})`));
488
- lines.push(` \u2717 ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);
489
- if (result.result !== void 0) {
490
- lines.push(` ${result.result.text.replace(/\s+/g, " ").slice(0, 200)}`);
491
- }
492
- }
493
- return lines.join("\n");
774
+ const lines = [];
775
+ const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);
776
+ lines.push(`ai-check: ${report.shown}/${report.total} shown, ${report.refused} refused, ${report.errors} failed${report.skipped > 0 ? `, ${report.skipped} skipped (no port)` : ""} — slowest ${slowest} ms`);
777
+ for (const reason of REFUSALS) {
778
+ const count = report.byRefusal[reason];
779
+ if (count > 0) lines.push(` ${reason}: ${count}`);
780
+ }
781
+ for (const result of report.results) {
782
+ if (result.ok) continue;
783
+ const why = result.refusal ?? `port failed (${result.error ?? "no message"})`;
784
+ lines.push(` ✗ ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);
785
+ if (result.result !== void 0) lines.push(` ${result.result.text.replace(/\s+/g, " ").slice(0, 200)}`);
786
+ }
787
+ return lines.join("\n");
494
788
  }
789
+ //#endregion
790
+ exports.aiCheckCases = aiCheckCases;
791
+ exports.formatAiCheckReport = formatAiCheckReport;
792
+ exports.runAiCheck = runAiCheck;
495
793
 
496
-
497
-
498
-
499
- exports.aiCheckCases = aiCheckCases; exports.formatAiCheckReport = formatAiCheckReport; exports.runAiCheck = runAiCheck;
500
794
  //# sourceMappingURL=ai-check.cjs.map