@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
@@ -1,1840 +0,0 @@
1
- /**
2
- * Configurable free-text answer matching (R3.3).
3
- *
4
- * Grade-stability contract: the DEFAULTS reproduce the v1 fill-in-the-blanks
5
- * semantics exactly (trim, then locale-insensitive lowercase, strict `===`).
6
- * Every tolerance below is opt-in, because changing a default here changes
7
- * historical grades — and this SDK powers real summative exams.
8
- */
9
- interface TextMatchPolicy {
10
- /** Compare case-sensitively. Default `false` (v1 behaviour). */
11
- caseSensitive?: boolean;
12
- /** Strip leading/trailing whitespace before comparing. Default `true` (v1 behaviour). */
13
- trim?: boolean;
14
- /**
15
- * Unicode normalization applied to both sides before comparing. Default
16
- * `'none'` (v1 behaviour). `'NFC'` makes a decomposed `está` (combining
17
- * U+0301) equal its composed form — the fix for accent-input mismatches in
18
- * EN/ES/PT content.
19
- */
20
- normalize?: 'none' | 'NFC' | 'NFKC';
21
- /** Treat diacritics as equal to their base letters (`está` ≡ `esta`). Default `false`. */
22
- foldDiacritics?: boolean;
23
- /** Collapse runs of inner whitespace to a single space. Default `false`. */
24
- collapseInnerWhitespace?: boolean;
25
- /** Ignore Unicode punctuation on both sides. Default `false`. */
26
- ignorePunctuation?: boolean;
27
- /** Maximum Levenshtein edit distance still accepted as a match. Default `0`. */
28
- levenshtein?: number;
29
- /**
30
- * BCP 47 tag for locale-aware case folding (e.g. `'tr'` for Turkish dotted /
31
- * dotless I). Default: locale-insensitive `toLowerCase()` (v1 behaviour).
32
- */
33
- locale?: string;
34
- }
35
- /**
36
- * How a match was achieved. Lets a consumer award full credit for an `exact`
37
- * match and partial credit for a `folded` or `fuzzy` one (e.g. diacritic
38
- * tolerance in a listening gap-fill vs. a spelling test).
39
- * - `exact` — equal under the baseline trim/case rules alone.
40
- * - `normalized` — required Unicode normalization, whitespace collapse, or punctuation tolerance.
41
- * - `folded` — required diacritic folding.
42
- * - `fuzzy` — required Levenshtein tolerance.
43
- * - `none` — no accepted answer matched.
44
- */
45
- interface TextMatchResult {
46
- matched: boolean;
47
- via: 'exact' | 'normalized' | 'folded' | 'fuzzy' | 'none';
48
- }
49
- /**
50
- * Levenshtein distance with an early-exit bound: returns `max + 1` as soon as
51
- * the distance provably exceeds `max`.
52
- */
53
- declare function levenshteinDistance(a: string, b: string, max: number): number;
54
- /**
55
- * Matches a learner's input against one or more accepted answers under a
56
- * {@link TextMatchPolicy}. Tolerances are evaluated in stages — baseline
57
- * (trim/case), then normalization, then diacritic folding, then Levenshtein —
58
- * and {@link TextMatchResult.via} reports the first stage that produced the
59
- * match, so graders can award credit by match quality.
60
- *
61
- * With no policy (or an empty one) this is byte-for-byte the v1 matching
62
- * semantics: `trim` + locale-insensitive `toLowerCase` + strict equality.
63
- */
64
- declare function matchText(input: string, accepted: string | readonly string[], policy?: TextMatchPolicy): TextMatchResult;
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
-
207
- /**
208
- * The contract between an activity and a model the HOST runs, for help a
209
- * learner reads: an explanation of a graded answer, and hints before one.
210
- *
211
- * The SDK never calls a model and holds no key, prompt or provider. It builds
212
- * the facts a model is given — from the item, the learner's answer and the
213
- * SDK's own scorer — and checks what comes back. The model explains a verdict
214
- * the SDK reached; it never reaches one.
215
- */
216
-
217
- /**
218
- * What an author allows AI to do for one item. Absent, or a feature left out,
219
- * leaves the choice to the deployment. `false` switches a feature off for this
220
- * item wherever it is delivered; an author can only switch a feature off,
221
- * never force one on.
222
- *
223
- * Switch hints off where any hint would give the answer away — a
224
- * one-word vocabulary item, say.
225
- */
226
- interface ActivityAiPermissions {
227
- /**
228
- * An explanation of the learner's graded answer — and, on a written
229
- * response, feedback on a draft, and on a read-aloud, coaching on the
230
- * marks: each is a model's words about the learner's own answer.
231
- */
232
- explanations?: boolean;
233
- /** Hints before the learner submits. */
234
- hints?: boolean;
235
- }
236
- /**
237
- * An activity as a renderer holds it: full data or a `redact()` projection.
238
- * Structural rather than the data union, so a renderer's own view of an item —
239
- * whose answer-key fields may be absent — is accepted as it is. The AI
240
- * functions read every field defensively.
241
- */
242
- interface AiActivityInput {
243
- readonly type: string;
244
- readonly id: string;
245
- readonly title: string;
246
- readonly locale?: unknown;
247
- readonly redacted?: unknown;
248
- readonly ai?: unknown;
249
- }
250
- /** The kinds of help a learner can be given. */
251
- type AiFeature = 'explanation' | 'hint' | 'writing-feedback' | 'pronunciation-coaching';
252
- /** The activity types the SDK builds facts for. */
253
- type AiSupportedActivityType = 'multiple-choice' | 'fill-in-the-blanks' | 'gap-select' | 'dictation';
254
- /**
255
- * A whole answer's grade, from the SDK's scorer. `category` is the verdict an
256
- * explanation must agree with: all marks, some, or none.
257
- */
258
- interface AiGrade {
259
- score: number;
260
- maxScore: number;
261
- passed: boolean;
262
- category: AiVerdict;
263
- }
264
- /** The three verdicts an explanation can speak to. */
265
- type AiVerdict = 'correct' | 'partly-correct' | 'incorrect';
266
- interface AiFactsBase {
267
- activityId: string;
268
- title: string;
269
- /** The language the item is written in, when the author gave one. */
270
- locale?: string;
271
- }
272
- /** One option of a multiple-choice question, and what the learner did with it. */
273
- interface AiOptionFact {
274
- id: string;
275
- text: string;
276
- chosen: boolean;
277
- /** Whether this option is part of the answer; `null` when the key is not available. */
278
- correct: boolean | null;
279
- /** The author's feedback on this option. */
280
- feedback?: string;
281
- }
282
- interface AiMultipleChoiceFacts extends AiFactsBase {
283
- activityType: 'multiple-choice';
284
- question: string;
285
- mode: 'single' | 'multi';
286
- options: AiOptionFact[];
287
- }
288
- /** One blank, in the order it appears in the passage. */
289
- interface AiBlankFact {
290
- id: string;
291
- /** 1-based, in passage order. */
292
- position: number;
293
- /** What the learner typed, or `''`. */
294
- typed: string;
295
- /** The answers that count; `null` when the key is not available. */
296
- accepted: string[] | null;
297
- /** The SDK's verdict on this blank; `null` before it is graded or without a key. */
298
- correct: boolean | null;
299
- /** The author's hint for this blank. */
300
- hint?: string;
301
- /** The author's feedback for this blank. */
302
- feedback?: string;
303
- }
304
- interface AiFillInTheBlanksFacts extends AiFactsBase {
305
- activityType: 'fill-in-the-blanks';
306
- /** The passage, each blank written as `[1]`, `[2]`… in passage order. */
307
- passage: string;
308
- blanks: AiBlankFact[];
309
- }
310
- /** One gap, in the order it appears in the passage. */
311
- interface AiGapFact {
312
- id: string;
313
- /** 1-based, in passage order. */
314
- position: number;
315
- /** The words offered at this gap. */
316
- choices: string[];
317
- /** The word the learner chose, or `null`. */
318
- chosen: string | null;
319
- /** The right word; `null` when the key is not available. */
320
- answer: string | null;
321
- /** The SDK's verdict on this gap; `null` before it is graded or without a key. */
322
- correct: boolean | null;
323
- /** The author's feedback for this gap. */
324
- feedback?: string;
325
- }
326
- interface AiGapSelectFacts extends AiFactsBase {
327
- activityType: 'gap-select';
328
- /** The passage, each gap written as `[1]`, `[2]`… in passage order. */
329
- passage: string;
330
- gaps: AiGapFact[];
331
- }
332
- /** One word of a dictation, as the SDK's alignment paired it. */
333
- interface AiWordFact {
334
- /** The word the recording says; `''` for an extra word the learner added. */
335
- expected: string;
336
- /** The word the learner typed; `''` for a word they left out. */
337
- typed: string;
338
- status: 'correct' | 'incorrect' | 'missing' | 'extra';
339
- }
340
- interface AiDictationFacts extends AiFactsBase {
341
- activityType: 'dictation';
342
- /** What the recording says; `null` when the key is not available. */
343
- transcript: string | null;
344
- /** What the learner typed. */
345
- typed: string;
346
- /** The alignment word by word; `null` when the key is not available. */
347
- words: AiWordFact[] | null;
348
- }
349
- /**
350
- * What a model is told about one item and one answer: the question as the
351
- * learner saw it, what they answered, and — part by part — the SDK's verdict.
352
- */
353
- type AiItemFacts = AiMultipleChoiceFacts | AiFillInTheBlanksFacts | AiGapSelectFacts | AiDictationFacts;
354
- /**
355
- * Asks for an explanation of a graded answer. Sent after the SDK has graded
356
- * it, so `grade` is always present, and an explanation that contradicts
357
- * `grade.category` is refused.
358
- */
359
- interface AiExplanationRequest {
360
- feature: 'explanation';
361
- facts: AiItemFacts;
362
- grade: AiGrade;
363
- /** The language to answer in: the learner's interface language, when known. */
364
- learnerLocale?: string;
365
- }
366
- /**
367
- * Asks for the next hint before the learner submits. `facts` carries the key
368
- * so a model can hint toward the answer; the SDK refuses any hint that
369
- * contains it.
370
- */
371
- interface AiHintRequest {
372
- feature: 'hint';
373
- facts: AiItemFacts;
374
- /** 1-based: the hint being asked for. */
375
- hintNumber: number;
376
- /** The hints already shown for this item, oldest first. */
377
- previousHints: string[];
378
- learnerLocale?: string;
379
- }
380
- /** What produced an AI text, for the record a host keeps. */
381
- interface AiProvenance {
382
- /** The model's identifier, e.g. a name and version. */
383
- model?: string;
384
- /** A hash or id of the prompt revision. */
385
- promptHash?: string;
386
- /** ISO 8601. */
387
- generatedAt?: string;
388
- }
389
- /**
390
- * What a host's port returns. Plain text: the SDK renders it as text, never
391
- * as HTML. An explanation may state the verdict it assumed; one that
392
- * disagrees with the SDK's is refused.
393
- */
394
- interface AiTextResult {
395
- text: string;
396
- /** For an explanation: the verdict the model explained. */
397
- verdict?: AiVerdict;
398
- provenance?: AiProvenance;
399
- /**
400
- * What the call cost, as the provider reported it to your port: the same
401
- * {@link GraderUsage} a returned grade carries, so one shape covers help for
402
- * a learner and marking by a grader.
403
- *
404
- * The SDK never estimates it and never adds it up. It carries what a port
405
- * sends through to `onInteraction`, beside the provenance, so the record of
406
- * what a learner was shown and the record of what it cost are the same
407
- * record. Numbers that are not finite and zero or more are dropped.
408
- */
409
- usage?: GraderUsage;
410
- }
411
- /**
412
- * Why a result was not shown. `misquotes-answer` is writing feedback that
413
- * corrects words the learner did not write: a correction whose quote is not in
414
- * the draft, or not where it claims to be. `contradicts-marks` is coaching on
415
- * a word the engine did not mark, or on a sound it did not report.
416
- */
417
- type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer' | 'misquotes-answer' | 'contradicts-marks';
418
- /** One criterion of a written response's rubric, as the author wrote it. */
419
- interface AiRubricCriterionFact {
420
- name: string;
421
- description?: string;
422
- weight: number;
423
- }
424
- /**
425
- * What a model is told about a draft of a written response: the task as the
426
- * learner saw it, the draft verbatim, and the rubric it will be graded on.
427
- */
428
- interface AiWritingFacts {
429
- activityType: 'written-response';
430
- activityId: string;
431
- title: string;
432
- /** The language the item is written in, when the author gave one. */
433
- locale?: string;
434
- prompt: string;
435
- /** The draft, exactly as the learner wrote it. Every correction quotes it. */
436
- text: string;
437
- /** Recomputed with `countWords`, as the grader will count it. */
438
- wordCount: number;
439
- minWords: number;
440
- maxWords: number;
441
- withinWordBounds: boolean;
442
- /** The rubric the answer will be graded on; `null` when the item has none. */
443
- rubric: AiRubricCriterionFact[] | null;
444
- /** The target language and level, when the author gave one (e.g. `"en-A2"`). */
445
- languageTarget?: string;
446
- }
447
- /**
448
- * Asks for feedback on a draft of a written response, before the learner
449
- * submits it. Practice only.
450
- */
451
- interface AiWritingFeedbackRequest {
452
- feature: 'writing-feedback';
453
- facts: AiWritingFacts;
454
- /** 1-based: which draft this is. */
455
- draftNumber: number;
456
- /** The text of the feedback already shown on earlier drafts, oldest first. */
457
- previousFeedback: string[];
458
- /** The language to write in: the learner's interface language, when known. */
459
- learnerLocale?: string;
460
- }
461
- /**
462
- * One correction a model proposes. `original` must be words the learner
463
- * wrote; the SDK finds them in the draft itself, so a model need not count
464
- * characters — leave `range` out unless you are sure of it, because a range
465
- * that does not hold the quote is refused.
466
- */
467
- interface AiWritingCorrection {
468
- original: string;
469
- corrected: string;
470
- explanation?: string;
471
- /** An error-type tag, e.g. `"tense"`, `"article"`. */
472
- category?: string;
473
- range?: {
474
- start: number;
475
- end: number;
476
- };
477
- }
478
- /**
479
- * A model's judgement of one rubric criterion. The SDK attaches the author's
480
- * weight; a weight sent here is ignored.
481
- */
482
- type AiCriterionJudgement = Omit<CriterionScore, 'weight'> & {
483
- weight?: number;
484
- };
485
- /**
486
- * What a host's `writingFeedback` port returns: the overall feedback as plain
487
- * text, and optionally corrections and a judgement per rubric criterion.
488
- */
489
- interface AiWritingFeedbackResult {
490
- /** The overall feedback, addressed to the learner. Plain text, never HTML. */
491
- text: string;
492
- corrections?: AiWritingCorrection[];
493
- criteria?: AiCriterionJudgement[];
494
- provenance?: AiProvenance;
495
- usage?: GraderUsage;
496
- }
497
- /**
498
- * Feedback on a draft, as the SDK accepted it: what a learner is shown.
499
- */
500
- interface AiWritingFeedback {
501
- text: string;
502
- /**
503
- * Every correction, anchored in the draft: `range` is where the SDK found
504
- * the quote, and `original` is the draft's own text at that range.
505
- */
506
- corrections: InlineCorrection[];
507
- /** The model's judgements, with the rubric's weights. */
508
- criteria: CriterionScore[];
509
- /**
510
- * The rubric's weighted total of the judgements, from 0 to 1, computed by
511
- * the SDK as `gradeFromRubric` computes a grade — when every criterion of
512
- * the rubric was judged; `null` otherwise, and on an item with no rubric.
513
- *
514
- * **An indication, never a grade:** it is a model's reading of a draft the
515
- * learner has not submitted, and nothing in the SDK scores an answer with
516
- * it. `ai-writing-feedback-shown` reports it, so a record can say what the
517
- * learner was shown.
518
- */
519
- indicativeScore: number | null;
520
- provenance?: AiProvenance;
521
- usage?: GraderUsage;
522
- }
523
- /** One sound of a word, as the engine reported it. */
524
- interface AiSoundFact {
525
- /** The sound expected, in the assessment's phoneme alphabet. */
526
- symbol: string;
527
- /** How close it came, 0..100, when the engine scored it. */
528
- accuracy?: number;
529
- /** What the engine heard instead, best first, each with its score 0..100. */
530
- heardAs?: {
531
- symbol: string;
532
- score: number;
533
- }[];
534
- }
535
- /** One word of a reading, as the engine marked it. */
536
- interface AiReadingWordFact {
537
- /** `w<n>` for a word of the text; absent for a word the learner added. */
538
- itemId?: string;
539
- /** The word of the text, normalised; `''` for a word the learner added. */
540
- word: string;
541
- /** What was heard, normalised; `''` for an omitted word. */
542
- heard: string;
543
- state: 'correct' | 'mispronounced' | 'omitted' | 'inserted';
544
- /** The engine's accuracy for the word, 0..100, when it has one. */
545
- accuracy?: number;
546
- /** The word's sounds, from the full assessment only. */
547
- sounds?: AiSoundFact[];
548
- }
549
- /**
550
- * What a model is told about a read-aloud: the text, the engine's marks word
551
- * by word, and its scores. The marks are the engine's; a model explains them
552
- * and never makes its own.
553
- */
554
- interface AiReadingFacts {
555
- activityType: 'read-aloud';
556
- activityId: string;
557
- title: string;
558
- /** The language the text is in. */
559
- locale: string;
560
- referenceText: string;
561
- instructions?: string;
562
- /** Who made the marks: a measurement engine, a model, or a person. */
563
- assessor: 'auto' | 'ai' | 'human' | 'unknown';
564
- /** The alphabet `sounds` are written in, when any are. */
565
- phonemeAlphabet?: 'ipa' | 'sapi';
566
- /** The engine's dimension scores, each 0..100, where it measured them. */
567
- scores: {
568
- accuracy?: number;
569
- fluency?: number;
570
- completeness?: number;
571
- prosody?: number;
572
- };
573
- words: AiReadingWordFact[];
574
- }
575
- /**
576
- * Asks for coaching on a graded reading: what to work on, word by word, and
577
- * how. Never in an exam.
578
- */
579
- interface AiCoachingRequest {
580
- feature: 'pronunciation-coaching';
581
- facts: AiReadingFacts;
582
- /** The grade of the reading, when there is one. */
583
- grade?: {
584
- score: number;
585
- maxScore: number;
586
- passed: boolean;
587
- };
588
- /** The language to write in: the learner's interface language, when known. */
589
- learnerLocale?: string;
590
- }
591
- /** One word a model coaches, as it returns it. */
592
- interface AiCoachingWord {
593
- /** The `itemId` of a word the engine marked mispronounced or omitted. */
594
- itemId: string;
595
- /** What to do about it, addressed to the learner. */
596
- tip: string;
597
- /**
598
- * A sound the engine reported for this word — `expected` — and, if the
599
- * model names one, what the engine heard instead.
600
- */
601
- sound?: {
602
- expected: string;
603
- heard?: string;
604
- };
605
- }
606
- /** What a host's `pronunciationCoaching` port returns. */
607
- interface AiCoachingResult {
608
- /** The coaching as a whole, addressed to the learner. Plain text, never HTML. */
609
- text: string;
610
- words?: AiCoachingWord[];
611
- provenance?: AiProvenance;
612
- usage?: GraderUsage;
613
- }
614
- /** Coaching, as the SDK accepted it: what a learner is shown. */
615
- interface AiCoaching {
616
- text: string;
617
- /**
618
- * The words to work on, in reading order, each with the word of the text as
619
- * the SDK has it — never the model's spelling of it.
620
- */
621
- words: (AiCoachingWord & {
622
- word: string;
623
- })[];
624
- provenance?: AiProvenance;
625
- usage?: GraderUsage;
626
- }
627
-
628
- /**
629
- * An xAPI Activity object — the thing a statement is about.
630
- * This SDK only builds Activity-type objects (xAPI 1.0.3 §4.1.4.1).
631
- */
632
- interface XAPIObject {
633
- objectType: 'Activity';
634
- /** IRI uniquely identifying the activity. */
635
- id: string;
636
- definition?: {
637
- /** Language-map of human-readable names for the activity. */
638
- name?: Record<string, string>;
639
- /** Language-map of human-readable descriptions. */
640
- description?: Record<string, string>;
641
- /** IRI identifying the activity type. */
642
- type?: string;
643
- /** xAPI 1.0.3 cmi.interaction type (e.g. `choice`, `fill-in`, `long-fill-in`). */
644
- interactionType?: string;
645
- /** Patterns describing the correct response(s), per the xAPI CRP format. */
646
- correctResponsesPattern?: string[];
647
- /** For `choice`-family interactions: the available components. */
648
- choices?: Array<{
649
- id: string;
650
- description?: Record<string, string>;
651
- }>;
652
- extensions?: Record<string, unknown>;
653
- };
654
- }
655
- /**
656
- * An xAPI Agent (person or system) that performed the statement's verb.
657
- * Exactly one of `mbox` or `account` must be present.
658
- */
659
- interface XAPIActor {
660
- objectType: 'Agent';
661
- /** Display name for the actor. */
662
- name?: string;
663
- /** Mailto IRI uniquely identifying the actor (e.g. `mailto:user@example.com`). */
664
- mbox?: string;
665
- /** Account-based actor identifier. */
666
- account?: {
667
- /** IRI of the home page of the account's service provider. */
668
- homePage: string;
669
- /** The actor's unique name on that service. */
670
- name: string;
671
- };
672
- }
673
- /** An xAPI Verb — the action performed in the statement. */
674
- interface XAPIVerbObject {
675
- /** IRI uniquely identifying this verb (e.g. from adlnet.gov/expapi/verbs). */
676
- id: string;
677
- /** Language-map of human-readable verb labels. */
678
- display: Record<string, string>;
679
- }
680
- /** Score sub-object within an xAPI Result. */
681
- interface XAPIScore {
682
- /** Normalized score in the range [0, 1]. */
683
- scaled: number;
684
- /** Raw score in the instrument's native scale. */
685
- raw?: number;
686
- /** Minimum possible raw score. */
687
- min?: number;
688
- /** Maximum possible raw score. */
689
- max?: number;
690
- }
691
- /** Result information attached to an xAPI Statement. */
692
- interface XAPIResult {
693
- score?: XAPIScore;
694
- /** Whether the actor's performance was successful. */
695
- success?: boolean;
696
- /** Whether the activity was completed. */
697
- completion?: boolean;
698
- /** ISO 8601 duration string representing time spent (e.g. `PT5M30S`). */
699
- duration?: string;
700
- /** A string representation of the learner's response. */
701
- response?: string;
702
- extensions?: Record<string, unknown>;
703
- }
704
- /** Related-activity references attached to a statement's context. */
705
- interface XAPIContextActivities {
706
- /** Activities this statement's activity is directly part of (e.g. the exam section). */
707
- parent?: XAPIObject[];
708
- /** Broader groupings (e.g. the course). */
709
- grouping?: XAPIObject[];
710
- /** Categorisations (e.g. a profile IRI). */
711
- category?: XAPIObject[];
712
- /** Other contextually related activities. */
713
- other?: XAPIObject[];
714
- }
715
- /** Context information attached to an xAPI Statement. */
716
- interface XAPIContext {
717
- /** Name of the software used to record the statement. */
718
- platform?: string;
719
- /** BCP 47 language tag for the activity content. */
720
- language?: string;
721
- /** Related activities (parent/grouping/category/other). */
722
- contextActivities?: XAPIContextActivities;
723
- extensions?: Record<string, unknown>;
724
- }
725
- /** A complete xAPI 1.0.3 Statement. */
726
- interface XAPIStatement {
727
- /** UUID v4 uniquely identifying this statement. */
728
- id: string;
729
- actor: XAPIActor;
730
- verb: XAPIVerbObject;
731
- object: XAPIObject;
732
- result?: XAPIResult;
733
- context?: XAPIContext;
734
- /** ISO 8601 timestamp of when the statement occurred. */
735
- timestamp: string;
736
- version: '1.0.3';
737
- }
738
- /**
739
- * Configuration passed to `useXAPI` to connect to an LRS endpoint.
740
- * The `auth` field supports both HTTP Basic and Bearer token authentication.
741
- */
742
- interface XAPIConfig {
743
- /** Full URL of the LRS statements endpoint. */
744
- endpoint: string;
745
- /** Authentication credentials for the LRS. */
746
- auth: {
747
- type: 'basic';
748
- username: string;
749
- password: string;
750
- } | {
751
- type: 'bearer';
752
- token: string;
753
- };
754
- /**
755
- * Replaces the SDK's placeholder object id (`urn:learning-kit:activity:*`)
756
- * before a statement is sent.
757
- *
758
- * - `string` — the IRI for THE activity this hook instance serves. A hook
759
- * instance is per-activity by contract; with several activities sharing
760
- * one hook, a plain string would collapse their statements onto one IRI —
761
- * use the function form instead.
762
- * - `(sdkObjectId) => string` — maps each placeholder id (which embeds the
763
- * activity's `data.id`) to its deployment IRI, for pages that send many
764
- * activities through one hook.
765
- *
766
- * Object ids the consumer already rewrote (non-`urn:learning-kit:` ids) are
767
- * never touched.
768
- */
769
- activityId: string | ((sdkObjectId: string) => string);
770
- actor: XAPIActor;
771
- /** Called after all retry attempts are exhausted. */
772
- onError?: (err: XAPIError) => void;
773
- }
774
- /** Error payload delivered to `XAPIConfig.onError` after retries are exhausted. */
775
- interface XAPIError {
776
- /** The statement that failed to send. */
777
- statement: XAPIStatement;
778
- /** The 1-based attempt number on which the final failure occurred. */
779
- attempt: number;
780
- /** HTTP status code from the LRS, if a response was received. */
781
- statusCode?: number;
782
- /** Human-readable error description. */
783
- message: string;
784
- }
785
-
786
- /**
787
- * Maps each ActivityType string to its corresponding data shape.
788
- *
789
- * Open for extension via TypeScript module augmentation: a consumer that
790
- * registers a custom activity type (`registerActivityType`) can augment this
791
- * interface so `ActivityType`, `ActivityData`, `validateActivity`, and
792
- * `evaluate` pick the new type up without an SDK release:
793
- *
794
- * ```ts
795
- * declare module '@intellectif/lk-core' {
796
- * interface ActivityDataMap { 'my-type': MyTypeData }
797
- * interface LearnerResponseMap { 'my-type': MyTypeLearnerResponse }
798
- * }
799
- * ```
800
- */
801
- interface ActivityDataMap {
802
- 'multiple-choice': MultipleChoiceData;
803
- 'fill-in-the-blanks': FillInTheBlanksData;
804
- 'written-response': WrittenResponseData;
805
- 'gap-select': GapSelectData;
806
- dictation: DictationData;
807
- 'read-aloud': ReadAloudData;
808
- }
809
- /**
810
- * The set of activity types supported by learning-kit. Derived from
811
- * {@link ActivityDataMap}, so module augmentation widens it automatically.
812
- */
813
- type ActivityType = keyof ActivityDataMap;
814
- /** Union of all valid activity data shapes. */
815
- type ActivityData = ActivityDataMap[ActivityType];
816
- /** A text track for an `audio` or `video` recording. */
817
- interface MediaTrack {
818
- kind: 'captions' | 'subtitles';
819
- /** WebVTT URL. A player may fetch it through a loader the host supplies, with the host's credentials. */
820
- src: string;
821
- /** BCP 47 language tag of the text (`en`, `es`, `pt-BR`). */
822
- srclang: string;
823
- /** Shown in the captions menu. Non-empty, at most 60 characters. */
824
- label: string;
825
- /** Selected when the learner has chosen no language yet. At most one track may be the default. */
826
- default?: boolean;
827
- }
828
- /**
829
- * Optional media (image/audio/video) shown above a question or passage.
830
- * URL-only: hosting/delivery is the consuming application's responsibility.
831
- */
832
- interface ActivityMedia {
833
- /** The kind of media; selects the rendered element. `embed` → sandboxed iframe. */
834
- type: 'image' | 'audio' | 'video' | 'embed';
835
- /**
836
- * Source URL. For `embed` this MUST be the provider's embeddable URL
837
- * (e.g. `https://www.youtube.com/embed/<id>`), not the watch page.
838
- */
839
- url: string;
840
- /** Alternative text. Required for `image` and `embed`; optional label for audio/video. */
841
- alt?: string;
842
- /** Optional WebVTT captions track URL for `audio`/`video`. */
843
- captionsUrl?: string;
844
- /**
845
- * Text tracks for `audio`/`video`, one per language: what `captionsUrl`
846
- * cannot say — a language, a label, more than one. When present, a player
847
- * that reads it ignores `captionsUrl`. At most 12, one `default`, and no two
848
- * sharing a `kind` and a `srclang`.
849
- */
850
- tracks?: MediaTrack[];
851
- /** An image shown before the first play. `video` only. */
852
- poster?: string;
853
- /**
854
- * How the recording may be played. `audio` only — see
855
- * {@link MediaPlaybackPolicy}. Absent means today's behaviour exactly: the
856
- * browser's own control bar, unlimited plays, free seeking and free speed.
857
- */
858
- playback?: MediaPlaybackPolicy;
859
- }
860
- /**
861
- * A hint to the browser's own control bar, emitted as a `controlsList` token.
862
- *
863
- * Advisory, and engine-dependent. `hide-download` removes a menu item where
864
- * `controlsList` is implemented; it never prevents a download, because the URL
865
- * is in the page and the bytes are in the network panel. The real control is a
866
- * short-lived signed URL, which belongs to the consuming application.
867
- */
868
- type NativeControlHint = 'hide-download' | 'hide-rate';
869
- /**
870
- * How an audio recording may be played.
871
- *
872
- * Every field is optional and every default reproduces the SDK's pre-0.8.0
873
- * behaviour, so adding this key is the only thing that changes anything.
874
- *
875
- * Setting any *enforcement* field (`maxPlays`, `seek: 'none'`, `rate: 'fixed'`)
876
- * resolves `controls` to `'minimal'`: the browser's bar cannot express a spent
877
- * budget — its play button stays enabled — so the SDK renders its own
878
- * transport rather than leave a control that looks operable and does nothing.
879
- */
880
- interface MediaPlaybackPolicy {
881
- /**
882
- * `native` renders the browser's control bar. `minimal` renders the SDK
883
- * transport. Resolved automatically; set it explicitly only to keep the
884
- * native bar on a recording that has nothing to enforce.
885
- */
886
- controls?: 'native' | 'minimal';
887
- /**
888
- * How many times the recording may be STARTED (1–20). A play is consumed
889
- * when playback begins from anywhere other than where it last stopped, so
890
- * pausing, resuming, and paging between the questions of one listening group
891
- * are free.
892
- *
893
- * The SDK refuses the play; it does not remember it. The count is durable
894
- * only if the consuming application persists it — see `mediaBudget` on
895
- * `<ActivitySequence>` in `@intellectif/lk-react`.
896
- */
897
- maxPlays?: number;
898
- /** `none` renders no scrubber and reverts an out-of-band seek. Resolves to `none` under a budget. */
899
- seek?: 'allow' | 'none';
900
- /** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
901
- rate?: 'allow' | 'fixed';
902
- /** Advisory hints to the native bar. See {@link NativeControlHint}. */
903
- nativeControlHints?: NativeControlHint[];
904
- }
905
- /**
906
- * Optional authored "overall feedback" shown after submission, chosen by
907
- * whether the learner passed (h5p-style overall feedback). Distinct from
908
- * per-option feedback; either field may be omitted.
909
- */
910
- interface ActivityFeedback {
911
- /** Shown when the learner passes (score ≥ pass threshold). */
912
- correct?: string;
913
- /** Shown when the learner does not pass. */
914
- incorrect?: string;
915
- }
916
- /** A single selectable option within a Multiple Choice activity. */
917
- /**
918
- * A picture or a recording carried by ONE multiple-choice option — the
919
- * A1/A2 picture-choice item, and the minimal-pair listening item.
920
- *
921
- * Narrower than {@link ActivityMedia}, which sits above the question, and
922
- * deliberately so. An option is a click target, and two of the activity-level
923
- * kinds cannot be one:
924
- *
925
- * - **`embed` is refused.** A provider iframe swallows pointer events, so
926
- * clicking the option would play the video instead of selecting the answer.
927
- * The learner could not choose it at all.
928
- * - **`video` is refused** for the same reason one step down: a native control
929
- * bar inside the option's label eats the clicks meant for the radio.
930
- * - **No `playback` policy.** `maxPlays` binds per slot through a
931
- * `MediaBudgetBinding`, and four recordings in one question raise a question
932
- * nothing has answered yet — whether the budget belongs to the option or to
933
- * the item. Until something asks, an option plays through the browser's own
934
- * bar and counts nothing.
935
- *
936
- * `alt` follows the same rule {@link ActivityMedia} uses, so there is only one
937
- * rule to learn: required and non-empty for an image, optional for audio, where
938
- * it becomes the player's accessible label.
939
- *
940
- * **On an image option, `alt` is part of the item.** It joins the option's
941
- * `text` in the radio's accessible name, so on "which picture shows a cat?" an
942
- * `alt` of "a cat" hands a screen-reader user the answer that a sighted learner
943
- * has to work out. That is a property of picture-choice items, not something a
944
- * schema can fix — the SDK requires `alt` so an option is never SILENTLY
945
- * inaccessible, and leaves the wording, and the item's validity, to the author.
946
- */
947
- interface MultipleChoiceOptionMedia {
948
- /** `image` for a picture-choice option, `audio` for a listening option. */
949
- type: 'image' | 'audio';
950
- /** Address of the file. Same scheme allow-list as {@link ActivityMedia}. */
951
- url: string;
952
- /** Required and non-empty for `image`; an optional accessible label for `audio`. */
953
- alt?: string;
954
- /** WebVTT captions for an audio option, rendered as a `<track>`. */
955
- captionsUrl?: string;
956
- }
957
- interface MultipleChoiceOption {
958
- /** Unique identifier for this option within the activity. */
959
- id: string;
960
- /**
961
- * Display text shown to the learner.
962
- *
963
- * Required even when the option carries {@link media}: it names the option in
964
- * the radio's accessible name and in the xAPI statement, and it is what the
965
- * learner sees if a picture fails to load. For a pure picture-choice option,
966
- * a neutral label ("Picture 1") keeps the naming out of the answer.
967
- */
968
- text: string;
969
- /** Whether this option is part of the correct answer. */
970
- isCorrect: boolean;
971
- /** Optional per-option feedback shown after submission. */
972
- feedback?: string;
973
- /** An optional picture or recording shown as part of this option. */
974
- media?: MultipleChoiceOptionMedia;
975
- }
976
- /** Data contract for a Multiple Choice activity. */
977
- interface MultipleChoiceData {
978
- schemaVersion: '1.0';
979
- type: 'multiple-choice';
980
- /** Unique identifier for this activity. */
981
- id: string;
982
- /** Human-readable title used in xAPI statements and error boundaries. */
983
- title: string;
984
- /** The question stem presented to the learner, as plain text. */
985
- question: string;
986
- /**
987
- * Optional sanitised rich-HTML rendering of the question, parallel to
988
- * `WrittenResponseData.promptHtml`. `<MultipleChoice>` renders it in place
989
- * of `question` when — and only when — a `sanitizeHtml` function is
990
- * supplied; without one the plain `question` is rendered and escaped.
991
- * Sanitisation is the application's responsibility: the SDK ships none.
992
- */
993
- questionHtml?: string;
994
- /** `single` allows one selection; `multi` allows multiple. */
995
- mode: 'single' | 'multi';
996
- /** Ordered list of answer options. */
997
- options: MultipleChoiceOption[];
998
- /** Scoring algorithm applied when the learner submits. */
999
- scoringStrategy: 'all-or-nothing' | 'partial';
1000
- /** Optional media shown above the question. */
1001
- media?: ActivityMedia;
1002
- /** Optional authored overall feedback shown after submission. */
1003
- feedback?: ActivityFeedback;
1004
- /** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1005
- passThreshold?: number;
1006
- /**
1007
- * When true, option order is shuffled deterministically.
1008
- *
1009
- * The order is derived from the renderer's `shuffleSeed` when one is given —
1010
- * pass the attempt id, and a review render reproduces exactly the order the
1011
- * learner sat. Without a seed the order is stable for the life of the mount
1012
- * only, and is NOT reproducible afterwards, so never grade or appeal against
1013
- * a remembered position.
1014
- */
1015
- shuffle?: boolean;
1016
- /** BCP 47 language tag for the activity content. */
1017
- locale?: string;
1018
- /** IRI references to learning objectives addressed by this activity. */
1019
- learningObjectives?: string[];
1020
- /** Subjective difficulty on a 1–5 scale. */
1021
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1022
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1023
- ai?: ActivityAiPermissions;
1024
- }
1025
- /** Configuration for a single fill-in-the-blank slot. */
1026
- interface BlankConfig {
1027
- /** Unique identifier matching the `{{blank_id}}` placeholder in the passage. */
1028
- id: string;
1029
- /** List of strings accepted as correct answers for this blank. */
1030
- acceptedAnswers: string[];
1031
- /** Whether answer matching is case-sensitive. Defaults to false. */
1032
- caseSensitive?: boolean;
1033
- /** Whether leading/trailing whitespace is stripped before matching. Defaults to true. */
1034
- trimWhitespace?: boolean;
1035
- /**
1036
- * Optional matching-tolerance policy for this blank (Unicode normalization,
1037
- * diacritic folding, whitespace collapse, punctuation tolerance, typo
1038
- * tolerance). Every tolerance is opt-in; when absent, matching reproduces
1039
- * the v1 trim + case-fold semantics exactly. Fields set here take
1040
- * precedence over the legacy `caseSensitive` / `trimWhitespace` flags.
1041
- */
1042
- match?: TextMatchPolicy;
1043
- /** Optional hint text revealed on learner request. */
1044
- hint?: string;
1045
- /** Optional feedback shown inline next to this blank after submission. */
1046
- feedback?: string;
1047
- }
1048
- /** Data contract for a Fill-in-the-Blanks activity. */
1049
- /** One selectable option in a Gap Select activity. */
1050
- interface GapSelectChoice {
1051
- /** Unique identifier within the choice set the gap resolves to. */
1052
- id: string;
1053
- /** The text the learner reads in the selector. */
1054
- text: string;
1055
- }
1056
- /**
1057
- * A named set of choices several gaps can draw from — a word bank.
1058
- *
1059
- * A bank is what makes distractors possible: three gaps sharing a bank of five
1060
- * choices means every selector offers all five, and two of them answer no gap
1061
- * at all. That is the difference between a reading-comprehension item and a
1062
- * sequence of three-way guesses, so it is modelled once here rather than
1063
- * retrofitted after per-gap lists ship.
1064
- */
1065
- interface GapSelectBank {
1066
- /** Unique identifier referenced by {@link GapSelectGap.bankId}. */
1067
- id: string;
1068
- /** The choices every gap on this bank offers. */
1069
- choices: GapSelectChoice[];
1070
- }
1071
- /**
1072
- * One gap in the passage, matching a `{{gap_id}}` placeholder.
1073
- *
1074
- * Its choices come from exactly one place: `choices` for a list of its own, or
1075
- * `bankId` for a shared word bank. Both, or neither, is an authoring error the
1076
- * schema rejects — with two sources a reader cannot tell which list the learner
1077
- * is offered, and the answer key means nothing without one.
1078
- */
1079
- interface GapSelectGap {
1080
- /** Unique identifier matching the `{{gap_id}}` placeholder in the passage. */
1081
- id: string;
1082
- /** This gap's own choices. Mutually exclusive with {@link bankId}. */
1083
- choices?: GapSelectChoice[];
1084
- /** The {@link GapSelectBank} this gap draws from. Mutually exclusive with {@link choices}. */
1085
- bankId?: string;
1086
- /** The `id` of the one choice that is correct. Must exist in the resolved choice set. */
1087
- correctChoiceId: string;
1088
- /** Optional feedback shown inline next to this gap after submission. */
1089
- feedback?: string;
1090
- }
1091
- /**
1092
- * Data contract for a Gap Select activity: a passage whose gaps the learner
1093
- * fills by choosing from a list rather than typing.
1094
- *
1095
- * It is deliberately **not** a mode of {@link FillInTheBlanksData}, though the
1096
- * passage and its `{{id}}` placeholders are authored the same way. A learner
1097
- * picking from a selector cannot mistype, so the whole `TextMatchPolicy`
1098
- * surface — diacritic folding, typo tolerance, locale-aware case — is not just
1099
- * unused but misleading. Scoring is identity comparison of a choice id.
1100
- * Redaction inverts too: in Fill-in-the-Blanks the candidate answers ARE the
1101
- * key, while here the learner must be shown every choice and only
1102
- * `correctChoiceId` is withheld.
1103
- */
1104
- interface GapSelectData {
1105
- schemaVersion: '1.0';
1106
- type: 'gap-select';
1107
- /** Unique identifier for this activity. */
1108
- id: string;
1109
- /** Human-readable title used in xAPI statements and error boundaries. */
1110
- title: string;
1111
- /** Passage text containing `{{gap_id}}` placeholders. */
1112
- passage: string;
1113
- /**
1114
- * Optional sanitised rich-HTML rendering of the passage. Carried and redacted
1115
- * as learner-visible content; not rendered by the SDK, for the same reason
1116
- * `FillInTheBlanksData.passageHtml` is not — slicing sanitised HTML at the
1117
- * placeholders to host the selectors is both lossy and unsafe.
1118
- */
1119
- passageHtml?: string;
1120
- /** Every gap in the passage, one per distinct `{{id}}` placeholder. */
1121
- gaps: GapSelectGap[];
1122
- /** Shared word banks. Only needed by gaps that set `bankId`. */
1123
- banks?: GapSelectBank[];
1124
- /** Scoring algorithm applied when the learner submits. */
1125
- scoringStrategy: 'all-or-nothing' | 'partial';
1126
- /**
1127
- * How the choices are presented. Only `'dropdown'` exists today, and it is
1128
- * the default.
1129
- *
1130
- * The field is here rather than assumed because a drag-and-drop presentation
1131
- * is the obvious next request, and WCAG 2.5.7 requires that a drag interface
1132
- * always keep a non-drag path — so the choice has to be expressible in the
1133
- * content, not decided by a component prop. `'drag'` is NOT accepted yet:
1134
- * shipping a value nothing renders would freeze an API this repository has
1135
- * not validated. Widening the union later is additive.
1136
- */
1137
- presentation?: 'dropdown';
1138
- /**
1139
- * Whether the SDK shuffles each selector's choices. Off by default.
1140
- *
1141
- * Like `MultipleChoiceData.shuffle`, this needs a seed to be reproducible at
1142
- * a remark — `<ActivitySequence>` refuses to shuffle without one outside
1143
- * `practice` mode.
1144
- */
1145
- shuffleChoices?: boolean;
1146
- /** Optional media shown above the passage. */
1147
- media?: ActivityMedia;
1148
- /** Optional authored overall feedback shown after submission. */
1149
- feedback?: ActivityFeedback;
1150
- /** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1151
- passThreshold?: number;
1152
- /** BCP 47 language tag for the activity content. */
1153
- locale?: string;
1154
- /** IRI references to learning objectives addressed by this activity. */
1155
- learningObjectives?: string[];
1156
- /** Subjective difficulty on a 1–5 scale. */
1157
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1158
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1159
- ai?: ActivityAiPermissions;
1160
- }
1161
- interface FillInTheBlanksData {
1162
- schemaVersion: '1.0';
1163
- type: 'fill-in-the-blanks';
1164
- /** Unique identifier for this activity. */
1165
- id: string;
1166
- /** Human-readable title used in xAPI statements and error boundaries. */
1167
- title: string;
1168
- /** Passage text containing `{{blank_id}}` placeholders. */
1169
- passage: string;
1170
- /**
1171
- * Optional sanitised rich-HTML rendering of the passage, parallel to
1172
- * `WrittenResponseData.promptHtml`. Carried and redacted as learner-visible
1173
- * content; not rendered by the SDK yet. Sanitisation is the application's.
1174
- */
1175
- passageHtml?: string;
1176
- /** Configuration for each blank in the passage. */
1177
- blanks: BlankConfig[];
1178
- /** Scoring algorithm applied when the learner submits. */
1179
- scoringStrategy: 'all-or-nothing' | 'partial';
1180
- /** Optional media shown above the passage. */
1181
- media?: ActivityMedia;
1182
- /** Optional authored overall feedback shown after submission. */
1183
- feedback?: ActivityFeedback;
1184
- /** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1185
- passThreshold?: number;
1186
- /** BCP 47 language tag for the activity content. */
1187
- locale?: string;
1188
- /** IRI references to learning objectives addressed by this activity. */
1189
- learningObjectives?: string[];
1190
- /** Subjective difficulty on a 1–5 scale. */
1191
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1192
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1193
- ai?: ActivityAiPermissions;
1194
- }
1195
- /** A single criterion within a written-response grading rubric. */
1196
- interface WrittenResponseRubricCriterion {
1197
- /** Criterion name (e.g. "Task achievement", "Grammar range"). */
1198
- name: string;
1199
- /** Optional longer description of what the criterion assesses. */
1200
- description?: string;
1201
- /** Non-negative weight of this criterion in the overall grade. */
1202
- weight: number;
1203
- }
1204
- /** Grading rubric attached to a written-response activity. */
1205
- interface WrittenResponseRubric {
1206
- /** Optional display label for the rubric as a whole. */
1207
- label?: string;
1208
- /** The criteria the response is graded against. Non-empty when present. */
1209
- criteria: WrittenResponseRubricCriterion[];
1210
- }
1211
- /**
1212
- * Data contract for a Written Response activity (free-text writing graded
1213
- * asynchronously — by an AI or human grader — after submission).
1214
- *
1215
- * Wire-format note (Req 22.9): field names and casing are locked for
1216
- * byte-compatibility with consumer-stored JSONB rows. `feedback` and
1217
- * `passThreshold` are SDK-side optional additions for component parity
1218
- * (Req 22.8 / Req 3.9) — being optional, their absence keeps stored payloads
1219
- * byte-identical.
1220
- */
1221
- interface WrittenResponseData {
1222
- schemaVersion: '1.0';
1223
- type: 'written-response';
1224
- /** Unique identifier for this activity. */
1225
- id: string;
1226
- /** Human-readable title used in xAPI statements and error boundaries. */
1227
- title: string;
1228
- /** The writing prompt, as plain text. */
1229
- prompt: string;
1230
- /**
1231
- * Optional sanitised rich-HTML sidecar of the prompt. `<WrittenResponse>`
1232
- * renders it in place of `prompt` when a `sanitizeHtml` function is
1233
- * supplied; without one the plain `prompt` is rendered and escaped.
1234
- */
1235
- promptHtml?: string;
1236
- /** Minimum acceptable word count (≥ 0). */
1237
- minWords: number;
1238
- /** Maximum acceptable word count (≥ 1, and ≥ `minWords`). */
1239
- maxWords: number;
1240
- /** Optional grading rubric consumed by the asynchronous grader. */
1241
- rubric?: WrittenResponseRubric;
1242
- /** Optional target language/level for the response (e.g. `"en-A2"`, `"es-B1"`). */
1243
- languageTarget?: string;
1244
- /** Optional media shown above the prompt. */
1245
- media?: ActivityMedia;
1246
- /** Optional authored overall feedback (surfaced once the deferred grade exists). */
1247
- feedback?: ActivityFeedback;
1248
- /** Minimum scaled score [0–1] required to pass once graded. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1249
- passThreshold?: number;
1250
- /** BCP 47 language tag for the activity content. */
1251
- locale?: string;
1252
- /** IRI references to learning objectives addressed by this activity. */
1253
- learningObjectives?: string[];
1254
- /** Subjective difficulty on a 1–5 scale. */
1255
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1256
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1257
- ai?: ActivityAiPermissions;
1258
- }
1259
- /**
1260
- * A second, slower recording of a dictation — a separate file (a text-to-speech
1261
- * render at reduced speed, or a slower human reading), not a `playbackRate`
1262
- * change. It carries no `captionsUrl` (a caption track of a dictation is the
1263
- * transcript on screen) and no `playback` policy of its own: it follows the
1264
- * policy on `media`, except the play budget, which it never has.
1265
- */
1266
- interface DictationSlowMedia {
1267
- type: 'audio';
1268
- /** Source URL, under the same policy as {@link ActivityMedia.url}. */
1269
- url: string;
1270
- /** Accessible label — a description ("Recording, slow"), never a transcription. */
1271
- alt?: string;
1272
- }
1273
- /**
1274
- * A whole-word rewrite applied to BOTH the transcript and the attempt before
1275
- * they are compared. `from` and `to` are literal text: a rule is applied to
1276
- * every whole-word occurrence, exactly once, in the order the rules are listed,
1277
- * after spacing is collapsed and before punctuation is ignored — so a rule may
1278
- * name a symbol (`{ from: '&', to: 'and' }`) and a multi-word `from` matches
1279
- * across any whitespace.
1280
- */
1281
- interface DictationEquivalence {
1282
- /** What to rewrite: at least one code point that survives case, quote and whitespace folding. */
1283
- from: string;
1284
- /** What it becomes: 1..200 code points, something of which survives normalisation. A rule cannot delete a word. */
1285
- to: string;
1286
- }
1287
- /** Grading tolerances for a dictation. Part of the answer key. */
1288
- interface DictationTolerance {
1289
- /** Whole-word rewrites, applied in listed order, each exactly once. */
1290
- equivalences?: DictationEquivalence[];
1291
- }
1292
- /**
1293
- * Data contract for a Dictation activity: the learner listens to a recording
1294
- * and types what they hear; the grade is how close their text is to the
1295
- * transcript.
1296
- *
1297
- * Scoring is character-level similarity over the whole sentence —
1298
- * `(length − edit distance) / length` over NFC code points after case,
1299
- * punctuation and spacing are ignored — with one {@link ScoringDetail} per
1300
- * transcript word carrying that word's own {@link ScoringDetail.score}. It is
1301
- * deliberately not a mode of {@link FillInTheBlanksData}: a blank is right or
1302
- * wrong, a dictation is graded on a continuum, and the whole `TextMatchPolicy`
1303
- * surface (typo tolerance, diacritic folding) would be a second, competing
1304
- * notion of "close enough".
1305
- */
1306
- interface DictationData {
1307
- schemaVersion: '1.0';
1308
- type: 'dictation';
1309
- /** Unique identifier for this activity. */
1310
- id: string;
1311
- /**
1312
- * Human-readable title used in xAPI statements and error boundaries. It is
1313
- * the stem the learner sees, so it must not contain the transcript.
1314
- */
1315
- title: string;
1316
- /**
1317
- * What the learner is expected to type. ANSWER KEY. 1..2000 code points,
1318
- * before and after its equivalences are applied, and something must survive
1319
- * normalisation.
1320
- */
1321
- transcript: string;
1322
- /**
1323
- * Alternative transcriptions accepted at full credit — a regional spelling, a
1324
- * numeral, a whole-sentence contraction variant. ANSWER KEY. At most 10, each
1325
- * 1..2000 code points, none normalising equal to the transcript or to each
1326
- * other. The attempt is scored against every candidate and the best
1327
- * similarity wins; a tie goes to `transcript`.
1328
- */
1329
- acceptedTranscripts?: string[];
1330
- /**
1331
- * The recording. Audio only, never with captions. Optional in the schema — a
1332
- * dictation inside an item group may draw on the group's stimulus recording —
1333
- * but required whenever `slowMedia` is present. Budgeted per slot like any
1334
- * other activity media, with the usual playback policy.
1335
- */
1336
- media?: ActivityMedia;
1337
- /** A second, slower recording. See {@link DictationSlowMedia}. */
1338
- slowMedia?: DictationSlowMedia;
1339
- /**
1340
- * Progressive word hints, in `practice` only (the transcript is absent in
1341
- * `exam` mode, so there is nothing to reveal). Public: it reveals nothing by
1342
- * itself. `progressive-words` reveals the transcript's words left to right,
1343
- * one per request; a revealed hint never changes the score.
1344
- */
1345
- hints?: {
1346
- mode: 'progressive-words';
1347
- };
1348
- /** Grading tolerances. ANSWER KEY. */
1349
- tolerance?: DictationTolerance;
1350
- /** Optional authored overall feedback shown after submission. */
1351
- feedback?: ActivityFeedback;
1352
- /** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1353
- passThreshold?: number;
1354
- /** BCP 47 language tag for the activity content. Presentation only: scoring never reads it. */
1355
- locale?: string;
1356
- /** IRI references to learning objectives addressed by this activity. */
1357
- learningObjectives?: string[];
1358
- /** Subjective difficulty on a 1–5 scale. */
1359
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1360
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1361
- ai?: ActivityAiPermissions;
1362
- }
1363
- /** A dimension of speech a pronunciation assessor scores, on a 0–100 scale. */
1364
- type ReadAloudDimension = 'accuracy' | 'fluency' | 'completeness' | 'prosody';
1365
- /** One assessor dimension counted towards a read-aloud grade, and its weight. */
1366
- interface ReadAloudDimensionWeight {
1367
- name: ReadAloudDimension;
1368
- /**
1369
- * Relative weight, 0..1000 (`READ_ALOUD_MAX_DIMENSION_WEIGHT`). Weights need
1370
- * not add up to anything; a weight of 0 leaves the dimension out of the grade.
1371
- */
1372
- weight: number;
1373
- }
1374
- /**
1375
- * The limits on a read-aloud take, applied where the learner records. No grade
1376
- * reads them: whether a take is plausible speech is decided from the server's
1377
- * own measurement of the recording, never from a bound the client enforced.
1378
- */
1379
- interface RecordingBounds {
1380
- /** The longest take, in seconds: above 0, and at most `READ_ALOUD_MAX_SECONDS` (300). */
1381
- maxSeconds: number;
1382
- /** The shortest take, in seconds: at least 0, and below `maxSeconds`. */
1383
- minSeconds?: number;
1384
- /** How many takes the learner may record: a whole number from 1 to `READ_ALOUD_MAX_TAKES` (20). */
1385
- maxTakes?: number;
1386
- }
1387
- /**
1388
- * A second, slower model recording of the reference text — a separate file,
1389
- * not a `playbackRate` change. It carries no `captionsUrl` and no `playback`
1390
- * policy of its own, requires `media`, must be a different file from it, and
1391
- * cannot accompany a play limit on `media`.
1392
- */
1393
- interface ReadAloudSlowMedia {
1394
- type: 'audio';
1395
- /** Source URL, under the same policy as {@link ActivityMedia.url}. */
1396
- url: string;
1397
- /** Accessible label — a description ("Model recording, slow"). */
1398
- alt?: string;
1399
- }
1400
- /**
1401
- * Data contract for a Read Aloud activity: the learner reads a text aloud, an
1402
- * assessor measures the recording, and the grade is the weighted total of the
1403
- * assessor's dimension scores.
1404
- *
1405
- * Grading is deferred — `evaluate()` returns `deferred` until the application
1406
- * holds an assessment — and nothing here is an answer key: the learner is shown
1407
- * the text they are asked to read.
1408
- */
1409
- interface ReadAloudData {
1410
- schemaVersion: '1.0';
1411
- type: 'read-aloud';
1412
- /** Unique identifier for this activity. */
1413
- id: string;
1414
- /** Human-readable title used in xAPI statements and error boundaries. */
1415
- title: string;
1416
- /** Optional instructions shown to the learner, as plain text. */
1417
- instructions?: string;
1418
- /**
1419
- * The text the learner reads aloud. Shown to the learner (public). At most
1420
- * `READ_ALOUD_MAX_REFERENCE_LENGTH` code points before and after
1421
- * normalisation, in a script written with spaces between words.
1422
- */
1423
- referenceText: string;
1424
- /**
1425
- * BCP 47 tag in canonical form with a region (e.g. `en-US`). GRADE-DECIDING:
1426
- * an assessment made for another locale is unscorable.
1427
- */
1428
- locale: string;
1429
- /** A model recording of the text. Audio only; a playback policy is allowed. */
1430
- media?: ActivityMedia;
1431
- /** A second, slower model recording. See {@link ReadAloudSlowMedia}. */
1432
- slowMedia?: ReadAloudSlowMedia;
1433
- /** The limits on a take. See {@link RecordingBounds}. */
1434
- recording: RecordingBounds;
1435
- /**
1436
- * The assessor dimensions the grade is made of, with their weights: 1 to 4
1437
- * entries, each name at most once, at least one weight above 0. A weighted
1438
- * dimension the assessment has no score for makes the take unscorable, never
1439
- * a zero. Public.
1440
- */
1441
- scoring: {
1442
- dimensions: ReadAloudDimensionWeight[];
1443
- };
1444
- /** Minimum scaled score [0–1] required to pass once graded. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
1445
- passThreshold?: number;
1446
- /** Optional authored overall feedback, selected once the grade exists. */
1447
- feedback?: ActivityFeedback;
1448
- /** IRI references to learning objectives addressed by this activity. */
1449
- learningObjectives?: string[];
1450
- /** Subjective difficulty on a 1–5 scale. */
1451
- difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1452
- /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1453
- ai?: ActivityAiPermissions;
1454
- }
1455
- /**
1456
- * Maps each ActivityType string to its learner-response shape. Open for
1457
- * extension via module augmentation, mirroring {@link ActivityDataMap}.
1458
- */
1459
- interface LearnerResponseMap {
1460
- 'multiple-choice': MultipleChoiceLearnerResponse;
1461
- 'fill-in-the-blanks': FillInTheBlanksLearnerResponse;
1462
- 'written-response': WrittenResponseLearnerResponse;
1463
- 'gap-select': GapSelectLearnerResponse;
1464
- dictation: DictationLearnerResponse;
1465
- 'read-aloud': ReadAloudLearnerResponse;
1466
- }
1467
- /** Union of all learner response shapes. */
1468
- type LearnerResponse = LearnerResponseMap[keyof LearnerResponseMap];
1469
- /** Learner response for a Multiple Choice activity. */
1470
- interface MultipleChoiceLearnerResponse {
1471
- type: 'multiple-choice';
1472
- /** IDs of the options the learner selected. */
1473
- selectedOptionIds: string[];
1474
- /**
1475
- * Hints the learner was shown before submitting this answer, counted from
1476
- * the start of the question — the author's and AI hints alike. Written by
1477
- * the SDK's components in `practice`; absent or 0 means none. A scoring
1478
- * policy's `hintPenalty` charges for them. Client-reported, like any count
1479
- * a browser makes.
1480
- */
1481
- hintsRevealed?: number;
1482
- }
1483
- /** Learner response for a Fill-in-the-Blanks activity. */
1484
- interface FillInTheBlanksLearnerResponse {
1485
- type: 'fill-in-the-blanks';
1486
- /** Map of blank ID to the learner's typed answer. */
1487
- answers: Record<string, string>;
1488
- /**
1489
- * Hints the learner was shown before submitting this answer, counted from
1490
- * the start of the question — the author's and AI hints alike. Written by
1491
- * the SDK's components in `practice`; absent or 0 means none. A scoring
1492
- * policy's `hintPenalty` charges for them. Client-reported, like any count
1493
- * a browser makes.
1494
- */
1495
- hintsRevealed?: number;
1496
- }
1497
- /**
1498
- * Learner response for a Gap Select activity.
1499
- *
1500
- * A gap the learner has not answered is **absent from the map, or holds an
1501
- * empty string** — the two are equivalent, because a `<select>` whose
1502
- * placeholder is still showing submits `''`. Neither is a wrong answer: the
1503
- * scorer reports it as `incorrect-omission`, which is how a blank the learner
1504
- * never reached is told apart from one they got wrong.
1505
- */
1506
- interface GapSelectLearnerResponse {
1507
- type: 'gap-select';
1508
- /** Map of gap ID to the ID of the choice the learner selected. */
1509
- selections: Record<string, string>;
1510
- /**
1511
- * Hints the learner was shown before submitting this answer, counted from
1512
- * the start of the question — the author's and AI hints alike. Written by
1513
- * the SDK's components in `practice`; absent or 0 means none. A scoring
1514
- * policy's `hintPenalty` charges for them. Client-reported, like any count
1515
- * a browser makes.
1516
- */
1517
- hintsRevealed?: number;
1518
- }
1519
- /** Learner response for a Written Response activity. */
1520
- interface WrittenResponseLearnerResponse {
1521
- type: 'written-response';
1522
- /** The learner's free-text response. */
1523
- text: string;
1524
- /** Word count of `text`, computed with the canonical `countWords()` helper. */
1525
- wordCount: number;
1526
- }
1527
- /**
1528
- * Learner response for a Dictation activity.
1529
- *
1530
- * `text` is exactly what the learner typed; every normalisation happens in the
1531
- * scorer. `hintsRevealed` is client-reported — how many hint words were shown
1532
- * before submitting. The scorer ignores it; a scoring policy's `hintPenalty`
1533
- * charges for it, knowing it cannot be verified.
1534
- */
1535
- interface DictationLearnerResponse {
1536
- type: 'dictation';
1537
- /** The learner's text, as typed. */
1538
- text: string;
1539
- /**
1540
- * Hint words shown before submitting (practice only), counted from the start
1541
- * of the question. Absent or 0 means none.
1542
- */
1543
- hintsRevealed?: number;
1544
- }
1545
- /**
1546
- * A learner recording held by the application. `key` is opaque,
1547
- * application-issued storage identity: never a URL or bytes. `durationMs` is a
1548
- * client claim no grade reads.
1549
- */
1550
- interface RecordingRef {
1551
- key: string;
1552
- /** The media type the recording was stored as, e.g. `audio/wav`. */
1553
- mimeType: string;
1554
- durationMs?: number;
1555
- }
1556
- /**
1557
- * Learner response for a Read Aloud activity: a reference to the recording the
1558
- * application stored, never the audio itself.
1559
- */
1560
- interface ReadAloudLearnerResponse {
1561
- type: 'read-aloud';
1562
- /** `null`: the learner submitted without recording (a blank). A take that failed to upload is never `null`. */
1563
- recording: RecordingRef | null;
1564
- /** Telemetry: takes used. No grade reads it. */
1565
- takes?: number;
1566
- }
1567
- /**
1568
- * Fine-grained outcome of the learner's action on a single item:
1569
- * - `correct` — the learner selected/entered the right answer.
1570
- * - `incorrect` — the learner selected/entered a wrong answer.
1571
- * - `correct-omission` — the learner correctly left a non-answer unselected (multiple-choice only).
1572
- * - `incorrect-omission` — a correct answer the learner did not give: an unselected
1573
- * correct option, an unanswered gap, a missing dictation word.
1574
- */
1575
- type ScoringOutcome = 'correct' | 'incorrect' | 'correct-omission' | 'incorrect-omission';
1576
- /** Per-item scoring breakdown returned by the scoring engine. */
1577
- interface ScoringDetail {
1578
- /** ID of the option, blank, gap or dictation word (`w1`…`wN`) this detail refers to. */
1579
- itemId: string;
1580
- /**
1581
- * The outcome of the learner's action on this item. On a multiple-choice
1582
- * option it says both whether the option was chosen and whether it is part
1583
- * of the answer.
1584
- *
1585
- * Until 1.0 a detail also carried `correct`, which on a multiple-choice
1586
- * option meant "the learner acted correctly on it" — an unselected wrong
1587
- * option read `correct: true` — and a detail stored before 0.3 carries only
1588
- * that. The SDK's components still read such a stored detail; a type written
1589
- * against 1.0 does not describe it.
1590
- */
1591
- outcome: ScoringOutcome;
1592
- /** The learner's actual response for this item. */
1593
- learnerResponse: string | string[];
1594
- /** The expected correct response(s) for this item. */
1595
- correctResponse: string | string[];
1596
- /**
1597
- * Weight applied to this item's contribution to the overall score. Every
1598
- * built-in scorer currently weights every item equally and writes `1`.
1599
- */
1600
- weight?: number;
1601
- /**
1602
- * The item's own scaled score in [0, 1], for a type that grades its items on
1603
- * a continuum rather than right or wrong. Written by the dictation scorer
1604
- * (the similarity of each transcript word to what was typed for it) and by
1605
- * `gradeReadAloud` (the assessor's accuracy for each reference word as a
1606
- * fraction, 0 for an omitted word, and absent where the assessor gave none).
1607
- * Absent, an item is right or wrong: read `outcome`.
1608
- */
1609
- score?: number;
1610
- }
1611
- /** Full scoring result returned by the scoring engine. */
1612
- interface ScoringResult {
1613
- /** Scaled score in the range [0, 1]. */
1614
- score: number;
1615
- /** Maximum possible scaled score (always 1). */
1616
- maxScore: number;
1617
- /** Whether the score meets or exceeds the activity's pass threshold. */
1618
- passed: boolean;
1619
- /**
1620
- * The authored overall feedback selected for this result: `feedback.correct`
1621
- * when the learner passed, `feedback.incorrect` otherwise; `null` when the
1622
- * activity authored no matching feedback.
1623
- */
1624
- feedback: string | null;
1625
- /** Per-item scoring breakdown. */
1626
- details: ScoringDetail[];
1627
- }
1628
- /**
1629
- * Progress facts about a deferred (asynchronously graded) submission that are
1630
- * computable synchronously at submit time. For written-response, both fields
1631
- * are always present and `wordCount` is recomputed from the submitted text
1632
- * with the canonical `countWords()` (the client-supplied count is not trusted).
1633
- */
1634
- interface DeferredScoringPartial {
1635
- /** Whether the recomputed word count falls within `[minWords, maxWords]`. */
1636
- withinWordBounds?: boolean;
1637
- /** Recomputed word count of the submitted text. */
1638
- wordCount?: number;
1639
- [key: string]: unknown;
1640
- }
1641
- /**
1642
- * Why an item has no grade *yet* — a state that is explicitly not final, and
1643
- * so keeps a composed assessment `provisional`.
1644
- *
1645
- * - `requires_async_grading` — the type is graded later by an AI or a human.
1646
- * - `no_response_recorded` — nothing was stored against this slot at all. It
1647
- * is NOT the same as a zero: a learner who left a question blank on a
1648
- * submitted paper has earned zero, but a slot missing because a save failed,
1649
- * a grade has not landed, or the attempt is still open has earned nothing
1650
- * yet, and recording it as zero is how an incomplete attempt becomes a
1651
- * plausible-looking fail.
1652
- * - `grade_rejected` — a grade came back, and `outcomeFromGrade` refused it
1653
- * because its numbers cannot be a grade (a `score` or `maxScore` that is not
1654
- * a finite number, a `maxScore` of 0 or less, or a score outside 0 to
1655
- * `maxScore`). The record is kept on `rejectedGrade`. A real grade is still
1656
- * owed, so re-grade the slot: waiting will not bring one.
1657
- */
1658
- type DeferredReason = 'requires_async_grading' | 'no_response_recorded' | 'grade_rejected';
1659
- /**
1660
- * The outcome of evaluating a learner response against an activity — the
1661
- * union `evaluate()` returns. Unlike {@link ScoringResult}, it can express
1662
- * "not gradable yet" (`deferred`) and "not gradable at all" (`unscorable`),
1663
- * so an ungraded written response is never conflated with a score of 0.
1664
- */
1665
- type ItemOutcome = {
1666
- status: 'scored';
1667
- /** Scaled score in the range [0, 1]. */
1668
- score: number;
1669
- /** Maximum possible scaled score. */
1670
- maxScore: number;
1671
- /** Whether the score meets or exceeds the activity's pass threshold. */
1672
- passed: boolean;
1673
- /** Authored overall feedback selected by pass state, or null. */
1674
- feedback: string | null;
1675
- /** Per-item scoring breakdown. */
1676
- details: ScoringDetail[];
1677
- } | {
1678
- status: 'deferred';
1679
- /** Why no grade exists yet. See {@link DeferredReason}. */
1680
- reason: DeferredReason;
1681
- /** Maximum possible scaled score once graded. */
1682
- maxScore: number;
1683
- /** Synchronously computable progress facts (word bounds, counts). */
1684
- partial?: DeferredScoringPartial;
1685
- /**
1686
- * The record `outcomeFromGrade` refused, verbatim, when `reason` is
1687
- * `grade_rejected` — kept for audit and for the re-grade. Nothing on it
1688
- * is mirrored onto the outcome, and nothing reads it as a grade.
1689
- */
1690
- rejectedGrade?: GradeRecord;
1691
- } | {
1692
- status: 'graded';
1693
- /**
1694
- * A grade produced by an asynchronous grader (AI or human) and handed
1695
- * back to the SDK. This is the state a `deferred` outcome transitions
1696
- * to once grading completes; nothing in the SDK ever manufactures it.
1697
- */
1698
- grade: GradeRecord;
1699
- /** Scaled score in the range [0, 1], mirrored from `grade` for uniform reads. */
1700
- score: number;
1701
- maxScore: number;
1702
- passed: boolean;
1703
- /** Narrative feedback from the grader, mirrored from `grade`. */
1704
- feedback: string | null;
1705
- } | {
1706
- status: 'unscorable';
1707
- /** Why no grade can be produced (e.g. unregistered activity type). */
1708
- reason: string;
1709
- /** Maximum possible scaled score, when known. */
1710
- maxScore: number;
1711
- /**
1712
- * A machine-readable reason, written by `outcomeFromUnscorable` — for
1713
- * example a read-aloud `SpeechUnscorableCode`. `evaluate` never writes it.
1714
- */
1715
- code?: string;
1716
- };
1717
- /** A single validation error produced by `validateActivity`. */
1718
- interface ValidationError {
1719
- /** JSON-path-style location of the invalid field. */
1720
- path: string[];
1721
- /** Human-readable description of the validation failure. */
1722
- message: string;
1723
- /** Machine-readable error code. */
1724
- code: string;
1725
- }
1726
- /**
1727
- * Result of a `validateActivity` call.
1728
- * On success, `data` is the validated and typed activity data.
1729
- * On failure, `errors` contains one entry per violated constraint.
1730
- */
1731
- type ValidationResult<T> = {
1732
- success: true;
1733
- data: T;
1734
- } | {
1735
- success: false;
1736
- errors: ValidationError[];
1737
- };
1738
- /** The payload delivered to an activity's `onComplete` callback. */
1739
- interface ActivityResult {
1740
- /** Scaled score in the range [0, 1]. */
1741
- score: number;
1742
- /** Maximum possible scaled score (always 1). */
1743
- maxScore: number;
1744
- /** Whether the learner passed based on the activity's pass threshold. */
1745
- passed: boolean;
1746
- /** Time in milliseconds from first interaction to submission. */
1747
- timeSpent: number;
1748
- /** The xAPI statement built and (optionally) sent for this attempt. */
1749
- xapiStatement: XAPIStatement;
1750
- }
1751
- /**
1752
- * The kinds of interaction the built-in components emit. Custom activity types
1753
- * registered by consumers may emit their own kinds, so any string is accepted;
1754
- * the named literals are kept for autocompletion.
1755
- */
1756
- type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'text-changed' | 'submitted'
1757
- /**
1758
- * A play was charged against a recording's budget, before any audio was
1759
- * audible. Emitted only when a budget is in force, so a `review`-mode replay
1760
- * never pollutes the record an appeal reads.
1761
- */
1762
- | 'media-play-consumed'
1763
- /** An exhausted budget refused a play. The artifact an appeal asks for. */
1764
- | 'media-play-refused'
1765
- /** A charged play produced no audio — a failed or expired media URL. */
1766
- | 'media-play-errored'
1767
- /** A charged play was returned, because the consumer accepted the refund. */
1768
- | 'media-play-refunded'
1769
- /** The microphone began capturing a take. */
1770
- | 'recording-started'
1771
- /** A take ended, by the learner or at its time limit. */
1772
- | 'recording-stopped'
1773
- /** A take was thrown away, to record another. */
1774
- | 'recording-discarded'
1775
- /** The application stored a take and returned its reference. */
1776
- | 'recording-uploaded'
1777
- /** Storing a take failed. A take that failed to upload is never a blank answer. */
1778
- | 'recording-upload-failed'
1779
- /** A stored take was sent for assessment. */
1780
- | 'assessment-requested'
1781
- /** An assessment request failed: the take has no grade. */
1782
- | 'assessment-failed'
1783
- /** Interactive video: the learner started playback. */
1784
- | 'video-played'
1785
- /** Interactive video: playback paused, by the learner or by a quiz. */
1786
- | 'video-paused'
1787
- /** Interactive video: the playhead moved (`from`, `to`, and whether navigation `clamped` it). */
1788
- | 'video-seeked' | 'video-rate-changed' | 'video-ended'
1789
- /** Interactive video: a quiz opened (`cueId`, `at`). */
1790
- | 'video-quiz-opened'
1791
- /** Interactive video: one question of an open quiz was shown (`cueId`, `slotId`). */
1792
- | 'video-quiz-question-shown' | 'video-quiz-skipped' | 'video-quiz-closed'
1793
- /**
1794
- * Interactive video: the captions a learner chose. `payload.srclang` is the
1795
- * primary line's language, or `null` with captions off; `payload.secondary` is
1796
- * the second line's, or `null` with none showing. Fires when either changes.
1797
- */
1798
- | 'video-captions-changed' | 'video-fullscreen-changed' | 'video-pip-changed'
1799
- /**
1800
- * An AI hint was shown to the learner (`hintNumber`, and the port's
1801
- * `provenance` when it sent one). Only a hint the SDK accepted: a refused one
1802
- * is never shown and never reported here.
1803
- */
1804
- | 'ai-hint-shown'
1805
- /** An AI explanation of the graded answer was shown (`provenance`, `usage` when sent). */
1806
- | 'ai-explanation-shown'
1807
- /**
1808
- * An AI answer was refused, and the learner was told help was not available
1809
- * (`feature`, `reason` — an {@link AiRefusal} — and `hintNumber` for a hint).
1810
- *
1811
- * **The text itself is never carried**: a hint refused for revealing the
1812
- * answer contains the answer, and this event is logged. It is how a host
1813
- * watches its own model in production, where the development console warning
1814
- * it accompanies is silent.
1815
- */
1816
- | 'ai-help-refused'
1817
- /**
1818
- * Feedback on a draft of a written response was shown (`draftNumber`, the
1819
- * number of `corrections`, the `indicativeScore` when there was one, and the
1820
- * port's `provenance` and `usage` when it sent them). Never the text.
1821
- */
1822
- | 'ai-writing-feedback-shown'
1823
- /**
1824
- * Coaching on a read-aloud was shown (the number of `words` it coached, and
1825
- * the port's `provenance` and `usage` when it sent them). Never the text.
1826
- */
1827
- | 'ai-coaching-shown' | (string & {});
1828
- /** Fired by activity components on every discrete learner interaction. */
1829
- interface InteractionEvent {
1830
- /** The kind of interaction that occurred. */
1831
- type: InteractionKind;
1832
- /** ID of the activity that produced the event. */
1833
- activityId: string;
1834
- /** Unix timestamp (ms) of when the interaction occurred. */
1835
- timestamp: number;
1836
- /** Additional event-specific data. */
1837
- payload: Record<string, unknown>;
1838
- }
1839
-
1840
- export { type GapSelectData as $, type ActivityAiPermissions as A, type AiReadingWordFact as B, type AiRefusal as C, type AiRubricCriterionFact as D, type AiSoundFact as E, type AiSupportedActivityType as F, type AiTextResult as G, type AiVerdict as H, type AiWordFact as I, type AiWritingCorrection as J, type AiWritingFacts as K, type AiWritingFeedback as L, type AiWritingFeedbackRequest as M, type AiWritingFeedbackResult as N, type BlankConfig as O, type CriterionScore as P, type DeferredReason as Q, type DeferredScoringPartial as R, type DictationData as S, type DictationEquivalence as T, type DictationLearnerResponse as U, type DictationSlowMedia as V, type DictationTolerance as W, type FillInTheBlanksData as X, type FillInTheBlanksLearnerResponse as Y, type GapSelectBank as Z, type GapSelectChoice as _, type ActivityData as a, type GapSelectGap as a0, type GapSelectLearnerResponse as a1, type GradeRecord as a2, type Grader as a3, type GraderKind as a4, type GraderUsage as a5, type GradingState as a6, type InlineCorrection as a7, type InteractionEvent as a8, type InteractionKind as a9, type WrittenResponseRubric as aA, type WrittenResponseRubricCriterion as aB, type XAPIActor as aC, type XAPIConfig as aD, type XAPIContext as aE, type XAPIContextActivities as aF, type XAPIError as aG, type XAPIObject as aH, type XAPIResult as aI, type XAPIScore as aJ, type XAPIStatement as aK, type XAPIVerbObject as aL, levenshteinDistance as aM, matchText as aN, type ItemOutcome as aa, type LearnerResponse as ab, type LearnerResponseMap as ac, type MediaPlaybackPolicy as ad, type MediaTrack as ae, type MultipleChoiceData as af, type MultipleChoiceLearnerResponse as ag, type MultipleChoiceOption as ah, type MultipleChoiceOptionMedia as ai, type NativeControlHint as aj, type ReadAloudData as ak, type ReadAloudDimension as al, type ReadAloudDimensionWeight as am, type ReadAloudLearnerResponse as an, type ReadAloudSlowMedia as ao, type RecordingBounds as ap, type RecordingRef as aq, type ScoringDetail as ar, type ScoringOutcome as as, type ScoringResult as at, type TextMatchPolicy as au, type TextMatchResult as av, type ValidationError as aw, type ValidationResult as ax, type WrittenResponseData as ay, type WrittenResponseLearnerResponse as az, type ActivityDataMap as b, type ActivityFeedback as c, type ActivityMedia as d, type ActivityResult as e, type ActivityType as f, type AiActivityInput as g, type AiBlankFact as h, type AiCoaching as i, type AiCoachingRequest as j, type AiCoachingResult as k, type AiCoachingWord as l, type AiCriterionJudgement as m, type AiDictationFacts as n, type AiExplanationRequest as o, type AiFeature as p, type AiFillInTheBlanksFacts as q, type AiGapFact as r, type AiGapSelectFacts as s, type AiGrade as t, type AiHintRequest as u, type AiItemFacts as v, type AiMultipleChoiceFacts as w, type AiOptionFact as x, type AiProvenance as y, type AiReadingFacts as z };