@intellectif/lk-core 0.18.0 → 0.20.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 +62 -0
- package/README.md +3 -1
- package/dist/{activity-HH8dxN1v.d.cts → activity-f0bgZSTP.d.cts} +163 -141
- package/dist/{activity-HH8dxN1v.d.ts → activity-f0bgZSTP.d.ts} +163 -141
- package/dist/ai-check.cjs +283 -0
- package/dist/ai-check.cjs.map +1 -0
- package/dist/ai-check.d.cts +106 -0
- package/dist/ai-check.d.ts +106 -0
- package/dist/ai-check.js +283 -0
- package/dist/ai-check.js.map +1 -0
- package/dist/chunk-BIJCO2HM.js +443 -0
- package/dist/chunk-BIJCO2HM.js.map +1 -0
- package/dist/chunk-FVZJ4X3Y.cjs +443 -0
- package/dist/chunk-FVZJ4X3Y.cjs.map +1 -0
- package/dist/{index-DejnxQpK.d.cts → index-7S3-TLjT.d.cts} +1 -1
- package/dist/{index-BrBBUb1i.d.ts → index-BA9f2ydg.d.ts} +1 -1
- package/dist/index.cjs +165 -428
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -5
- package/dist/index.d.ts +149 -5
- package/dist/index.js +167 -430
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/package.json +11 -1
|
@@ -63,6 +63,147 @@ declare function levenshteinDistance(a: string, b: string, max: number): number;
|
|
|
63
63
|
*/
|
|
64
64
|
declare function matchText(input: string, accepted: string | readonly string[], policy?: TextMatchPolicy): TextMatchResult;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The return trip for deferred grading.
|
|
68
|
+
*
|
|
69
|
+
* `evaluate()` can say a submission is `deferred` — graded later by an AI or a
|
|
70
|
+
* human — but until now there was no type for the grade that comes BACK, so
|
|
71
|
+
* every consumer invented one. The shapes here are the intersection of two
|
|
72
|
+
* independent production graders (a rubric-based essay grader and a CEFR
|
|
73
|
+
* speaking grader) that converged on the same envelope: a normalised total,
|
|
74
|
+
* per-criterion scores with comments, grader-authored artefacts, a confidence
|
|
75
|
+
* signal, an explicit human-review flag, and provenance.
|
|
76
|
+
*
|
|
77
|
+
* The SDK models the SHAPE and the arithmetic. It never calls a model, never
|
|
78
|
+
* holds a key, and never decides what a rubric means — that stays yours.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lifecycle of a deferred grade. A grade exists only in the `graded` state;
|
|
83
|
+
* every other state means "no grade, and here is why" — which is precisely the
|
|
84
|
+
* distinction that stops an ungraded submission being rendered as a zero.
|
|
85
|
+
*/
|
|
86
|
+
type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
|
|
87
|
+
/**
|
|
88
|
+
* Who produced a grade.
|
|
89
|
+
*
|
|
90
|
+
* - `'auto'` — a non-generative measurement engine, for example a
|
|
91
|
+
* pronunciation-assessment service.
|
|
92
|
+
* - `'ai'` — a generative model.
|
|
93
|
+
* - `'human'` — a person.
|
|
94
|
+
*/
|
|
95
|
+
type GraderKind = 'ai' | 'human' | 'auto';
|
|
96
|
+
/** Provenance of a grade, so a re-grade two years later is explicable. */
|
|
97
|
+
interface Grader {
|
|
98
|
+
kind: GraderKind;
|
|
99
|
+
/** Identifier of the human grader, when `kind` is `'human'`. */
|
|
100
|
+
id?: string;
|
|
101
|
+
/** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
|
|
102
|
+
model?: string;
|
|
103
|
+
/** Hash or id of the prompt/rubric revision used, for auditability. */
|
|
104
|
+
promptHash?: string;
|
|
105
|
+
}
|
|
106
|
+
/** Optional cost/usage telemetry emitted by an AI grader. */
|
|
107
|
+
interface GraderUsage {
|
|
108
|
+
promptTokens?: number;
|
|
109
|
+
completionTokens?: number;
|
|
110
|
+
costUsd?: number;
|
|
111
|
+
}
|
|
112
|
+
/** A grader's verdict on one rubric criterion. */
|
|
113
|
+
interface CriterionScore {
|
|
114
|
+
/** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
|
|
115
|
+
name: string;
|
|
116
|
+
/**
|
|
117
|
+
* The grader's score for this criterion, out of {@link CriterionScore.maxScore}
|
|
118
|
+
* (which defaults to 1, the SDK's scaled convention). Absent when the
|
|
119
|
+
* criterion is judged on an ordinal `band`.
|
|
120
|
+
*/
|
|
121
|
+
score?: number;
|
|
122
|
+
/**
|
|
123
|
+
* What `score` is out of. Defaults to `1`.
|
|
124
|
+
*
|
|
125
|
+
* Real graders rarely work in [0,1]: rubrics come back out of 100, out of 9
|
|
126
|
+
* (CEFR/IELTS bands), or out of a points total that differs between
|
|
127
|
+
* criteria. Before this field the only way to use {@link gradeFromRubric}
|
|
128
|
+
* was to normalise by hand first — and a grader that skipped that step got
|
|
129
|
+
* its scores REJECTED, which pushed integrators back to letting the model
|
|
130
|
+
* compute the weighted total itself. That is precisely the arithmetic this
|
|
131
|
+
* SDK exists to take away from a language model.
|
|
132
|
+
*
|
|
133
|
+
* Set it per criterion, so a rubric mixing a /100 criterion with a /9 band
|
|
134
|
+
* score still composes. `gradeFromRubric` normalises `score / maxScore`
|
|
135
|
+
* before weighting; the resulting ratio must land in [0,1].
|
|
136
|
+
*/
|
|
137
|
+
maxScore?: number;
|
|
138
|
+
/** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
|
|
139
|
+
band?: string;
|
|
140
|
+
/** The grader's comment for this criterion, addressed to the learner. */
|
|
141
|
+
comment?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Weight actually applied, echoed from the rubric so the total is checkable.
|
|
144
|
+
* Defaults to `1`. Zero or more: `gradeFromRubric` refuses a negative weight
|
|
145
|
+
* rather than let it carry the total outside [0,1].
|
|
146
|
+
*/
|
|
147
|
+
weight?: number;
|
|
148
|
+
/** Not applicable to this submission (e.g. interaction on a monologue task). */
|
|
149
|
+
notApplicable?: boolean;
|
|
150
|
+
}
|
|
151
|
+
/** A suggested correction anchored in the learner's own text. */
|
|
152
|
+
interface InlineCorrection {
|
|
153
|
+
/** Verbatim span from the submission. */
|
|
154
|
+
original: string;
|
|
155
|
+
/** Suggested replacement. */
|
|
156
|
+
corrected: string;
|
|
157
|
+
explanation?: string;
|
|
158
|
+
/** Character offsets into the submitted text, so a UI can anchor it inline. */
|
|
159
|
+
range?: {
|
|
160
|
+
start: number;
|
|
161
|
+
end: number;
|
|
162
|
+
};
|
|
163
|
+
/** Error-type tag (`"article"`, `"tense"`, `"register"`). */
|
|
164
|
+
category?: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A grade that came back from an asynchronous grader. `score` is scaled
|
|
168
|
+
* [0,1] against `maxScore`, matching every other score in the SDK. A grader
|
|
169
|
+
* that works in points does not have to normalise by hand: declare
|
|
170
|
+
* `maxScore` on each {@link CriterionScore} and let `gradeFromRubric` do the
|
|
171
|
+
* arithmetic, which is the whole point of handing it the judgements.
|
|
172
|
+
*
|
|
173
|
+
* `maxScore` is a positive, finite number and `score` a finite number from 0
|
|
174
|
+
* to it. `outcomeFromGrade` refuses a record that is not, rather than mirror
|
|
175
|
+
* numbers nobody can defend onto an outcome.
|
|
176
|
+
*/
|
|
177
|
+
interface GradeRecord {
|
|
178
|
+
score: number;
|
|
179
|
+
maxScore: number;
|
|
180
|
+
passed: boolean;
|
|
181
|
+
/** Narrative feedback addressed to the learner. */
|
|
182
|
+
feedback: string | null;
|
|
183
|
+
/** Per-criterion breakdown, when the grader worked against a rubric. */
|
|
184
|
+
criteria?: CriterionScore[];
|
|
185
|
+
/**
|
|
186
|
+
* Per-item marks carried by a grade — for example the per-word marks of a
|
|
187
|
+
* read-aloud grade. `outcomeFromGrade` does not mirror them onto the
|
|
188
|
+
* outcome; read `outcome.grade.details`.
|
|
189
|
+
*/
|
|
190
|
+
details?: ScoringDetail[];
|
|
191
|
+
/** Corrections anchored in the learner's text. */
|
|
192
|
+
corrections?: InlineCorrection[];
|
|
193
|
+
/** Supporting observations the grader cited. */
|
|
194
|
+
evidence?: string[];
|
|
195
|
+
/** How the grader reached this verdict. */
|
|
196
|
+
rationale?: string;
|
|
197
|
+
/** The grader's own confidence — a signal for routing to human review. */
|
|
198
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
199
|
+
/** The grader is unsure or the submission is atypical; route to a human. */
|
|
200
|
+
requiresHumanReview?: boolean;
|
|
201
|
+
grader?: Grader;
|
|
202
|
+
usage?: GraderUsage;
|
|
203
|
+
/** ISO 8601 timestamp of when the grade was produced. */
|
|
204
|
+
gradedAt?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
66
207
|
/**
|
|
67
208
|
* The contract between an activity and a model the HOST runs, for help a
|
|
68
209
|
* learner reads: an explanation of a graded answer, and hints before one.
|
|
@@ -72,6 +213,7 @@ declare function matchText(input: string, accepted: string | readonly string[],
|
|
|
72
213
|
* SDK's own scorer — and checks what comes back. The model explains a verdict
|
|
73
214
|
* the SDK reached; it never reaches one.
|
|
74
215
|
*/
|
|
216
|
+
|
|
75
217
|
/**
|
|
76
218
|
* What an author allows AI to do for one item. Absent, or a feature left out,
|
|
77
219
|
* leaves the choice to the deployment. `false` switches a feature off for this
|
|
@@ -250,150 +392,20 @@ interface AiTextResult {
|
|
|
250
392
|
/** For an explanation: the verdict the model explained. */
|
|
251
393
|
verdict?: AiVerdict;
|
|
252
394
|
provenance?: AiProvenance;
|
|
253
|
-
}
|
|
254
|
-
/** Why a result was not shown. */
|
|
255
|
-
type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* The return trip for deferred grading.
|
|
259
|
-
*
|
|
260
|
-
* `evaluate()` can say a submission is `deferred` — graded later by an AI or a
|
|
261
|
-
* human — but until now there was no type for the grade that comes BACK, so
|
|
262
|
-
* every consumer invented one. The shapes here are the intersection of two
|
|
263
|
-
* independent production graders (a rubric-based essay grader and a CEFR
|
|
264
|
-
* speaking grader) that converged on the same envelope: a normalised total,
|
|
265
|
-
* per-criterion scores with comments, grader-authored artefacts, a confidence
|
|
266
|
-
* signal, an explicit human-review flag, and provenance.
|
|
267
|
-
*
|
|
268
|
-
* The SDK models the SHAPE and the arithmetic. It never calls a model, never
|
|
269
|
-
* holds a key, and never decides what a rubric means — that stays yours.
|
|
270
|
-
*/
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Lifecycle of a deferred grade. A grade exists only in the `graded` state;
|
|
274
|
-
* every other state means "no grade, and here is why" — which is precisely the
|
|
275
|
-
* distinction that stops an ungraded submission being rendered as a zero.
|
|
276
|
-
*/
|
|
277
|
-
type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
|
|
278
|
-
/**
|
|
279
|
-
* Who produced a grade.
|
|
280
|
-
*
|
|
281
|
-
* - `'auto'` — a non-generative measurement engine, for example a
|
|
282
|
-
* pronunciation-assessment service.
|
|
283
|
-
* - `'ai'` — a generative model.
|
|
284
|
-
* - `'human'` — a person.
|
|
285
|
-
*/
|
|
286
|
-
type GraderKind = 'ai' | 'human' | 'auto';
|
|
287
|
-
/** Provenance of a grade, so a re-grade two years later is explicable. */
|
|
288
|
-
interface Grader {
|
|
289
|
-
kind: GraderKind;
|
|
290
|
-
/** Identifier of the human grader, when `kind` is `'human'`. */
|
|
291
|
-
id?: string;
|
|
292
|
-
/** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
|
|
293
|
-
model?: string;
|
|
294
|
-
/** Hash or id of the prompt/rubric revision used, for auditability. */
|
|
295
|
-
promptHash?: string;
|
|
296
|
-
}
|
|
297
|
-
/** Optional cost/usage telemetry emitted by an AI grader. */
|
|
298
|
-
interface GraderUsage {
|
|
299
|
-
promptTokens?: number;
|
|
300
|
-
completionTokens?: number;
|
|
301
|
-
costUsd?: number;
|
|
302
|
-
}
|
|
303
|
-
/** A grader's verdict on one rubric criterion. */
|
|
304
|
-
interface CriterionScore {
|
|
305
|
-
/** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
|
|
306
|
-
name: string;
|
|
307
|
-
/**
|
|
308
|
-
* The grader's score for this criterion, out of {@link CriterionScore.maxScore}
|
|
309
|
-
* (which defaults to 1, the SDK's scaled convention). Absent when the
|
|
310
|
-
* criterion is judged on an ordinal `band`.
|
|
311
|
-
*/
|
|
312
|
-
score?: number;
|
|
313
395
|
/**
|
|
314
|
-
* What
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* (CEFR/IELTS bands), or out of a points total that differs between
|
|
318
|
-
* criteria. Before this field the only way to use {@link gradeFromRubric}
|
|
319
|
-
* was to normalise by hand first — and a grader that skipped that step got
|
|
320
|
-
* its scores REJECTED, which pushed integrators back to letting the model
|
|
321
|
-
* compute the weighted total itself. That is precisely the arithmetic this
|
|
322
|
-
* SDK exists to take away from a language model.
|
|
396
|
+
* What the call cost, as the provider reported it to your port: the same
|
|
397
|
+
* {@link GraderUsage} a returned grade carries, so one shape covers help for
|
|
398
|
+
* a learner and marking by a grader.
|
|
323
399
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
400
|
+
* The SDK never estimates it and never adds it up. It carries what a port
|
|
401
|
+
* sends through to `onInteraction`, beside the provenance, so the record of
|
|
402
|
+
* what a learner was shown and the record of what it cost are the same
|
|
403
|
+
* record. Numbers that are not finite and zero or more are dropped.
|
|
327
404
|
*/
|
|
328
|
-
maxScore?: number;
|
|
329
|
-
/** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
|
|
330
|
-
band?: string;
|
|
331
|
-
/** The grader's comment for this criterion, addressed to the learner. */
|
|
332
|
-
comment?: string;
|
|
333
|
-
/**
|
|
334
|
-
* Weight actually applied, echoed from the rubric so the total is checkable.
|
|
335
|
-
* Defaults to `1`. Zero or more: `gradeFromRubric` refuses a negative weight
|
|
336
|
-
* rather than let it carry the total outside [0,1].
|
|
337
|
-
*/
|
|
338
|
-
weight?: number;
|
|
339
|
-
/** Not applicable to this submission (e.g. interaction on a monologue task). */
|
|
340
|
-
notApplicable?: boolean;
|
|
341
|
-
}
|
|
342
|
-
/** A suggested correction anchored in the learner's own text. */
|
|
343
|
-
interface InlineCorrection {
|
|
344
|
-
/** Verbatim span from the submission. */
|
|
345
|
-
original: string;
|
|
346
|
-
/** Suggested replacement. */
|
|
347
|
-
corrected: string;
|
|
348
|
-
explanation?: string;
|
|
349
|
-
/** Character offsets into the submitted text, so a UI can anchor it inline. */
|
|
350
|
-
range?: {
|
|
351
|
-
start: number;
|
|
352
|
-
end: number;
|
|
353
|
-
};
|
|
354
|
-
/** Error-type tag (`"article"`, `"tense"`, `"register"`). */
|
|
355
|
-
category?: string;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* A grade that came back from an asynchronous grader. `score` is scaled
|
|
359
|
-
* [0,1] against `maxScore`, matching every other score in the SDK. A grader
|
|
360
|
-
* that works in points does not have to normalise by hand: declare
|
|
361
|
-
* `maxScore` on each {@link CriterionScore} and let `gradeFromRubric` do the
|
|
362
|
-
* arithmetic, which is the whole point of handing it the judgements.
|
|
363
|
-
*
|
|
364
|
-
* `maxScore` is a positive, finite number and `score` a finite number from 0
|
|
365
|
-
* to it. `outcomeFromGrade` refuses a record that is not, rather than mirror
|
|
366
|
-
* numbers nobody can defend onto an outcome.
|
|
367
|
-
*/
|
|
368
|
-
interface GradeRecord {
|
|
369
|
-
score: number;
|
|
370
|
-
maxScore: number;
|
|
371
|
-
passed: boolean;
|
|
372
|
-
/** Narrative feedback addressed to the learner. */
|
|
373
|
-
feedback: string | null;
|
|
374
|
-
/** Per-criterion breakdown, when the grader worked against a rubric. */
|
|
375
|
-
criteria?: CriterionScore[];
|
|
376
|
-
/**
|
|
377
|
-
* Per-item marks carried by a grade — for example the per-word marks of a
|
|
378
|
-
* read-aloud grade. `outcomeFromGrade` does not mirror them onto the
|
|
379
|
-
* outcome; read `outcome.grade.details`.
|
|
380
|
-
*/
|
|
381
|
-
details?: ScoringDetail[];
|
|
382
|
-
/** Corrections anchored in the learner's text. */
|
|
383
|
-
corrections?: InlineCorrection[];
|
|
384
|
-
/** Supporting observations the grader cited. */
|
|
385
|
-
evidence?: string[];
|
|
386
|
-
/** How the grader reached this verdict. */
|
|
387
|
-
rationale?: string;
|
|
388
|
-
/** The grader's own confidence — a signal for routing to human review. */
|
|
389
|
-
confidence?: 'high' | 'medium' | 'low';
|
|
390
|
-
/** The grader is unsure or the submission is atypical; route to a human. */
|
|
391
|
-
requiresHumanReview?: boolean;
|
|
392
|
-
grader?: Grader;
|
|
393
405
|
usage?: GraderUsage;
|
|
394
|
-
/** ISO 8601 timestamp of when the grade was produced. */
|
|
395
|
-
gradedAt?: string;
|
|
396
406
|
}
|
|
407
|
+
/** Why a result was not shown. */
|
|
408
|
+
type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
|
|
397
409
|
|
|
398
410
|
/**
|
|
399
411
|
* An xAPI Activity object — the thing a statement is about.
|
|
@@ -1549,8 +1561,18 @@ type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled'
|
|
|
1549
1561
|
* is never shown and never reported here.
|
|
1550
1562
|
*/
|
|
1551
1563
|
| 'ai-hint-shown'
|
|
1552
|
-
/** An AI explanation of the graded answer was shown (`provenance` when sent). */
|
|
1553
|
-
| 'ai-explanation-shown'
|
|
1564
|
+
/** An AI explanation of the graded answer was shown (`provenance`, `usage` when sent). */
|
|
1565
|
+
| 'ai-explanation-shown'
|
|
1566
|
+
/**
|
|
1567
|
+
* An AI answer was refused, and the learner was told help was not available
|
|
1568
|
+
* (`feature`, `reason` — an {@link AiRefusal} — and `hintNumber` for a hint).
|
|
1569
|
+
*
|
|
1570
|
+
* **The text itself is never carried**: a hint refused for revealing the
|
|
1571
|
+
* answer contains the answer, and this event is logged. It is how a host
|
|
1572
|
+
* watches its own model in production, where the development console warning
|
|
1573
|
+
* it accompanies is silent.
|
|
1574
|
+
*/
|
|
1575
|
+
| 'ai-help-refused' | (string & {});
|
|
1554
1576
|
/** Fired by activity components on every discrete learner interaction. */
|
|
1555
1577
|
interface InteractionEvent {
|
|
1556
1578
|
/** The kind of interaction that occurred. */
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
var _chunkFVZJ4X3Ycjs = require('./chunk-FVZJ4X3Y.cjs');
|
|
7
|
+
require('./chunk-COBK2RUX.cjs');
|
|
8
|
+
require('./chunk-D4TOPSHV.cjs');
|
|
9
|
+
|
|
10
|
+
// src/ai-check/cases.ts
|
|
11
|
+
var capital = {
|
|
12
|
+
schemaVersion: "1.0",
|
|
13
|
+
type: "multiple-choice",
|
|
14
|
+
id: "ai-check-mc",
|
|
15
|
+
title: "Capital cities",
|
|
16
|
+
question: "What is the capital of Spain?",
|
|
17
|
+
mode: "single",
|
|
18
|
+
scoringStrategy: "all-or-nothing",
|
|
19
|
+
options: [
|
|
20
|
+
{ id: "madrid", text: "Madrid", isCorrect: true },
|
|
21
|
+
{ id: "barcelona", text: "Barcelona", isCorrect: false },
|
|
22
|
+
{ id: "seville", text: "Seville", isCorrect: false }
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
var introduction = {
|
|
26
|
+
schemaVersion: "1.0",
|
|
27
|
+
type: "fill-in-the-blanks",
|
|
28
|
+
id: "ai-check-fib",
|
|
29
|
+
title: "Introducing yourself",
|
|
30
|
+
passage: "My name {{be}} Rossi and I {{live}} in Madrid.",
|
|
31
|
+
blanks: [
|
|
32
|
+
{ id: "be", acceptedAnswers: ["is"] },
|
|
33
|
+
{ id: "live", acceptedAnswers: ["live"], hint: "Present simple, first person." }
|
|
34
|
+
],
|
|
35
|
+
scoringStrategy: "partial"
|
|
36
|
+
};
|
|
37
|
+
var landscape = {
|
|
38
|
+
schemaVersion: "1.0",
|
|
39
|
+
type: "gap-select",
|
|
40
|
+
id: "ai-check-gap",
|
|
41
|
+
title: "Geography",
|
|
42
|
+
locale: "es-ES",
|
|
43
|
+
passage: "El r\xEDo pasa por un {{g1}} muy profundo, y el agua {{g2}} fr\xEDa.",
|
|
44
|
+
gaps: [
|
|
45
|
+
{
|
|
46
|
+
id: "g1",
|
|
47
|
+
choices: [
|
|
48
|
+
{ id: "canon", text: "ca\xF1\xF3n" },
|
|
49
|
+
{ id: "campo", text: "campo" }
|
|
50
|
+
],
|
|
51
|
+
correctChoiceId: "canon"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "g2",
|
|
55
|
+
choices: [
|
|
56
|
+
{ id: "esta", text: "est\xE1" },
|
|
57
|
+
{ id: "es", text: "es" }
|
|
58
|
+
],
|
|
59
|
+
correctChoiceId: "esta"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
scoringStrategy: "partial"
|
|
63
|
+
};
|
|
64
|
+
var listening = {
|
|
65
|
+
schemaVersion: "1.0",
|
|
66
|
+
type: "dictation",
|
|
67
|
+
id: "ai-check-dictation",
|
|
68
|
+
title: "Listen and type",
|
|
69
|
+
transcript: "The train leaves at half past nine."
|
|
70
|
+
};
|
|
71
|
+
var chose = (ids) => ({
|
|
72
|
+
type: "multiple-choice",
|
|
73
|
+
selectedOptionIds: ids
|
|
74
|
+
});
|
|
75
|
+
var typed = (answers) => ({
|
|
76
|
+
type: "fill-in-the-blanks",
|
|
77
|
+
answers
|
|
78
|
+
});
|
|
79
|
+
var picked = (choices) => ({
|
|
80
|
+
type: "gap-select",
|
|
81
|
+
selections: choices
|
|
82
|
+
});
|
|
83
|
+
var wrote = (text) => ({ type: "dictation", text });
|
|
84
|
+
function explanationCase(id, about, data, response) {
|
|
85
|
+
const request = _chunkFVZJ4X3Ycjs.aiExplanationRequest.call(void 0, { data, response });
|
|
86
|
+
if (request === null) {
|
|
87
|
+
throw new Error(`ai-check: no explanation request for "${id}"`);
|
|
88
|
+
}
|
|
89
|
+
return { id, feature: "explanation", about, request };
|
|
90
|
+
}
|
|
91
|
+
function hintCase(id, about, data, response, previousHints = []) {
|
|
92
|
+
const request = _chunkFVZJ4X3Ycjs.aiHintRequest.call(void 0, { data, response, previousHints });
|
|
93
|
+
if (request === null) {
|
|
94
|
+
throw new Error(`ai-check: no hint request for "${id}"`);
|
|
95
|
+
}
|
|
96
|
+
return { id, feature: "hint", about, request };
|
|
97
|
+
}
|
|
98
|
+
function aiCheckCases() {
|
|
99
|
+
return [
|
|
100
|
+
explanationCase("mc-correct", "Chose the right city", capital, chose(["madrid"])),
|
|
101
|
+
explanationCase("mc-wrong", "Chose Barcelona", capital, chose(["barcelona"])),
|
|
102
|
+
explanationCase("mc-blank", "Chose nothing and submitted", capital, chose([])),
|
|
103
|
+
explanationCase(
|
|
104
|
+
"fib-correct",
|
|
105
|
+
"Filled both blanks correctly",
|
|
106
|
+
introduction,
|
|
107
|
+
typed({ be: "is", live: "live" })
|
|
108
|
+
),
|
|
109
|
+
explanationCase(
|
|
110
|
+
"fib-partly",
|
|
111
|
+
"One blank right, one wrong",
|
|
112
|
+
introduction,
|
|
113
|
+
typed({ be: "is", live: "lives" })
|
|
114
|
+
),
|
|
115
|
+
explanationCase(
|
|
116
|
+
"fib-wrong",
|
|
117
|
+
"Both blanks wrong",
|
|
118
|
+
introduction,
|
|
119
|
+
typed({ be: "are", live: "living" })
|
|
120
|
+
),
|
|
121
|
+
explanationCase(
|
|
122
|
+
"gap-partly",
|
|
123
|
+
"One gap right, one wrong, in Spanish",
|
|
124
|
+
landscape,
|
|
125
|
+
picked({ g1: "canon", g2: "es" })
|
|
126
|
+
),
|
|
127
|
+
explanationCase(
|
|
128
|
+
"gap-wrong",
|
|
129
|
+
"Both gaps wrong, in Spanish",
|
|
130
|
+
landscape,
|
|
131
|
+
picked({ g1: "campo", g2: "es" })
|
|
132
|
+
),
|
|
133
|
+
explanationCase(
|
|
134
|
+
"dictation-near",
|
|
135
|
+
"Typed the sentence with two words wrong",
|
|
136
|
+
listening,
|
|
137
|
+
wrote("The train leaves at half past five")
|
|
138
|
+
),
|
|
139
|
+
explanationCase(
|
|
140
|
+
"dictation-correct",
|
|
141
|
+
"Typed the sentence correctly",
|
|
142
|
+
listening,
|
|
143
|
+
wrote("The train leaves at half past nine.")
|
|
144
|
+
),
|
|
145
|
+
hintCase("mc-hint-1", "Has not answered yet", capital, chose([])),
|
|
146
|
+
hintCase("mc-hint-2", "Asked again after choosing Seville", capital, chose(["seville"]), [
|
|
147
|
+
"Think about where the government sits."
|
|
148
|
+
]),
|
|
149
|
+
hintCase("fib-hint-1", "Both blanks empty", introduction, typed({})),
|
|
150
|
+
hintCase(
|
|
151
|
+
"fib-hint-2",
|
|
152
|
+
"Asked again with one blank wrong",
|
|
153
|
+
introduction,
|
|
154
|
+
typed({ be: "are", live: "" }),
|
|
155
|
+
["Look again at the first blank."]
|
|
156
|
+
),
|
|
157
|
+
hintCase("gap-hint-1", "Neither gap chosen, in Spanish", landscape, picked({})),
|
|
158
|
+
hintCase(
|
|
159
|
+
"gap-hint-2",
|
|
160
|
+
"Asked again with the first gap wrong",
|
|
161
|
+
landscape,
|
|
162
|
+
picked({ g1: "campo" }),
|
|
163
|
+
["Piensa en la forma del terreno."]
|
|
164
|
+
)
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/ai-check/index.ts
|
|
169
|
+
var REFUSALS = [
|
|
170
|
+
"malformed",
|
|
171
|
+
"empty",
|
|
172
|
+
"too-long",
|
|
173
|
+
"contradicts-grade",
|
|
174
|
+
"reveals-answer"
|
|
175
|
+
];
|
|
176
|
+
var now = () => Date.now();
|
|
177
|
+
async function runCase(ports, one) {
|
|
178
|
+
const started = now();
|
|
179
|
+
const explaining = one.request.feature === "explanation";
|
|
180
|
+
const port = explaining ? ports.explain : ports.hint;
|
|
181
|
+
if (port === void 0) {
|
|
182
|
+
throw new Error(`ai-check: no port for "${one.id}"`);
|
|
183
|
+
}
|
|
184
|
+
let raw;
|
|
185
|
+
try {
|
|
186
|
+
raw = await port(one.request);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
case: one,
|
|
190
|
+
ok: false,
|
|
191
|
+
error: error instanceof Error ? error.message : String(error),
|
|
192
|
+
ms: now() - started
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const ms = now() - started;
|
|
196
|
+
const checked = explaining ? _chunkFVZJ4X3Ycjs.checkAiExplanation.call(void 0, raw, one.request) : _chunkFVZJ4X3Ycjs.checkAiHint.call(void 0, raw, one.request);
|
|
197
|
+
if (checked.ok) {
|
|
198
|
+
return { case: one, ok: true, result: checked.result, ms };
|
|
199
|
+
}
|
|
200
|
+
const text = typeof _optionalChain([raw, 'optionalAccess', _ => _.text]) === "string" ? raw : void 0;
|
|
201
|
+
return {
|
|
202
|
+
case: one,
|
|
203
|
+
ok: false,
|
|
204
|
+
refusal: checked.refusal,
|
|
205
|
+
...text !== void 0 ? { result: text } : {},
|
|
206
|
+
ms
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function runAiCheck(ports, options = {}) {
|
|
210
|
+
const all = _nullishCoalesce(options.cases, () => ( aiCheckCases()));
|
|
211
|
+
const asked = Math.trunc(_nullishCoalesce(options.concurrency, () => ( 1)));
|
|
212
|
+
const lanes = Number.isFinite(asked) && asked >= 1 ? Math.min(asked, 16) : 1;
|
|
213
|
+
const runnable = all.filter(
|
|
214
|
+
(one) => one.request.feature === "explanation" ? ports.explain !== void 0 : ports.hint !== void 0
|
|
215
|
+
);
|
|
216
|
+
const results = new Array(runnable.length);
|
|
217
|
+
let next = 0;
|
|
218
|
+
const lane = async () => {
|
|
219
|
+
while (next < runnable.length) {
|
|
220
|
+
const mine = next;
|
|
221
|
+
next += 1;
|
|
222
|
+
const one = runnable[mine];
|
|
223
|
+
if (one === void 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
results[mine] = await runCase(ports, one);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
await Promise.all(Array.from({ length: Math.min(lanes, runnable.length) }, lane));
|
|
230
|
+
const byRefusal = Object.fromEntries(REFUSALS.map((reason) => [reason, 0]));
|
|
231
|
+
let shown = 0;
|
|
232
|
+
let refused = 0;
|
|
233
|
+
let errors = 0;
|
|
234
|
+
for (const result of results) {
|
|
235
|
+
if (result.ok) {
|
|
236
|
+
shown += 1;
|
|
237
|
+
} else if (result.refusal !== void 0) {
|
|
238
|
+
refused += 1;
|
|
239
|
+
byRefusal[result.refusal] += 1;
|
|
240
|
+
} else {
|
|
241
|
+
errors += 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
total: results.length,
|
|
246
|
+
shown,
|
|
247
|
+
refused,
|
|
248
|
+
errors,
|
|
249
|
+
skipped: all.length - runnable.length,
|
|
250
|
+
byRefusal,
|
|
251
|
+
results
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function formatAiCheckReport(report) {
|
|
255
|
+
const lines = [];
|
|
256
|
+
const slowest = report.results.reduce((worst, one) => Math.max(worst, one.ms), 0);
|
|
257
|
+
lines.push(
|
|
258
|
+
`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`
|
|
259
|
+
);
|
|
260
|
+
for (const reason of REFUSALS) {
|
|
261
|
+
const count = report.byRefusal[reason];
|
|
262
|
+
if (count > 0) {
|
|
263
|
+
lines.push(` ${reason}: ${count}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const result of report.results) {
|
|
267
|
+
if (result.ok) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const why = _nullishCoalesce(result.refusal, () => ( `port failed (${_nullishCoalesce(result.error, () => ( "no message"))})`));
|
|
271
|
+
lines.push(` \u2717 ${result.case.id} [${result.case.feature}] ${result.case.about}: ${why}`);
|
|
272
|
+
if (result.result !== void 0) {
|
|
273
|
+
lines.push(` ${result.result.text.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
exports.aiCheckCases = aiCheckCases; exports.formatAiCheckReport = formatAiCheckReport; exports.runAiCheck = runAiCheck;
|
|
283
|
+
//# sourceMappingURL=ai-check.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","../src/ai-check/cases.ts","../src/ai-check/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/ai-check.cjs","sourcesContent":[null,"import { aiExplanationRequest, aiHintRequest } from '../ai.js';\nimport type {\n DictationData,\n FillInTheBlanksData,\n GapSelectData,\n LearnerResponse,\n MultipleChoiceData,\n} from '../types/activity.js';\nimport type { AiExplanationRequest, AiHintRequest } from '../types/ai.js';\n\n/**\n * The items the cases are built from. Small, ordinary language-course content\n * — and deliberately awkward in the places a model slips:\n *\n * - an answer a hint can hardly avoid naming (`Madrid`);\n * - an answer of one short word (`is`), which a hint may write as an ordinary\n * word and must not write beside its neighbour in the passage;\n * - an accented answer (`cañón`), so a hint that spells it without the accent\n * is still caught;\n * - a passage with more than one blank, so a hint can be about the wrong one.\n */\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 * One call to make against a host's port, and what a learner was looking at\n * when the SDK would have made it.\n */\nexport interface AiCheckCase {\n /** Stable across releases, so a host can name one in an allow-list or a report. */\n id: string;\n /** Which port the case is for. */\n feature: 'explanation' | 'hint';\n /** What the learner did, in a few words, for a report a person reads. */\n about: string;\n /** The request, built by the SDK exactly as a component builds it. */\n request: AiExplanationRequest | AiHintRequest;\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 });\n\n/** Builds a case, failing loudly rather than shipping a case with no request. */\nfunction explanationCase(\n id: string,\n about: string,\n data: Parameters<typeof aiExplanationRequest>[0]['data'],\n response: LearnerResponse,\n): AiCheckCase {\n const request = aiExplanationRequest({ data, response });\n if (request === null) {\n throw new Error(`ai-check: no explanation request for \"${id}\"`);\n }\n return { id, feature: 'explanation', about, request };\n}\n\nfunction hintCase(\n id: string,\n about: string,\n data: Parameters<typeof aiHintRequest>[0]['data'],\n response: LearnerResponse,\n previousHints: string[] = [],\n): AiCheckCase {\n const request = aiHintRequest({ data, response, previousHints });\n if (request === null) {\n throw new Error(`ai-check: no hint request for \"${id}\"`);\n }\n return { id, feature: 'hint', about, request };\n}\n\n/**\n * The cases {@link runAiCheck} runs: every type the SDK explains, answered\n * right, wrong and — where a type can be — partly right; and every type it\n * hints for, before an answer and after a wrong one.\n *\n * They are ordinary calls. What makes them a test is that the SDK knows the\n * answer to each, so it can check what your model writes about it.\n *\n * A fresh array each call, so a caller may filter, slice or add to it without\n * changing what the next caller gets.\n */\nexport function aiCheckCases(): AiCheckCase[] {\n return [\n explanationCase('mc-correct', 'Chose the right city', capital, chose(['madrid'])),\n explanationCase('mc-wrong', 'Chose Barcelona', capital, chose(['barcelona'])),\n explanationCase('mc-blank', 'Chose nothing and submitted', capital, chose([])),\n explanationCase(\n 'fib-correct',\n 'Filled both blanks correctly',\n introduction,\n typed({ be: 'is', live: 'live' }),\n ),\n explanationCase(\n 'fib-partly',\n 'One blank right, one wrong',\n introduction,\n typed({ be: 'is', live: 'lives' }),\n ),\n explanationCase(\n 'fib-wrong',\n 'Both blanks wrong',\n introduction,\n typed({ be: 'are', live: 'living' }),\n ),\n explanationCase(\n 'gap-partly',\n 'One gap right, one wrong, in Spanish',\n landscape,\n picked({ g1: 'canon', g2: 'es' }),\n ),\n explanationCase(\n 'gap-wrong',\n 'Both gaps wrong, in Spanish',\n landscape,\n picked({ g1: 'campo', g2: 'es' }),\n ),\n explanationCase(\n 'dictation-near',\n 'Typed the sentence with two words wrong',\n listening,\n wrote('The train leaves at half past five'),\n ),\n explanationCase(\n 'dictation-correct',\n 'Typed the sentence correctly',\n listening,\n wrote('The train leaves at half past nine.'),\n ),\n hintCase('mc-hint-1', 'Has not answered yet', capital, chose([])),\n hintCase('mc-hint-2', 'Asked again after choosing Seville', capital, chose(['seville']), [\n 'Think about where the government sits.',\n ]),\n hintCase('fib-hint-1', 'Both blanks empty', introduction, typed({})),\n hintCase(\n 'fib-hint-2',\n 'Asked again with one blank wrong',\n introduction,\n typed({ be: 'are', live: '' }),\n ['Look again at the first blank.'],\n ),\n hintCase('gap-hint-1', 'Neither gap chosen, in Spanish', landscape, picked({})),\n hintCase(\n 'gap-hint-2',\n 'Asked again with the first gap wrong',\n landscape,\n picked({ g1: 'campo' }),\n ['Piensa en la forma del terreno.'],\n ),\n ];\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 * });\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.\n */\nimport { checkAiExplanation, checkAiHint } from '../ai.js';\nimport type { AiExplanationRequest, AiHintRequest, AiRefusal, AiTextResult } 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}\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 /** 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];\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 explaining = one.request.feature === 'explanation';\n const port = explaining ? ports.explain : ports.hint;\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 const checked = explaining\n ? checkAiExplanation(raw, one.request as AiExplanationRequest)\n : checkAiHint(raw, one.request as AiHintRequest);\n if (checked.ok) {\n return { case: one, ok: true, result: checked.result, ms };\n }\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: checked.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) =>\n one.request.feature === 'explanation' ? ports.explain !== undefined : ports.hint !== undefined,\n );\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"]}
|