@intellectif/lk-core 0.16.0 → 0.18.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 (48) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +2 -2
  3. package/dist/{activity-DcxtpX7Z.d.cts → activity-HH8dxN1v.d.cts} +234 -4
  4. package/dist/{activity-DcxtpX7Z.d.ts → activity-HH8dxN1v.d.ts} +234 -4
  5. package/dist/{chunk-FJJ2GVBA.cjs → chunk-2CV6NU32.cjs} +19 -19
  6. package/dist/{chunk-FJJ2GVBA.cjs.map → chunk-2CV6NU32.cjs.map} +1 -1
  7. package/dist/{chunk-6P6XK2LI.cjs → chunk-COBK2RUX.cjs} +74 -39
  8. package/dist/chunk-COBK2RUX.cjs.map +1 -0
  9. package/dist/{chunk-IRUD73IQ.js → chunk-D3GP5MET.js} +683 -638
  10. package/dist/chunk-D3GP5MET.js.map +1 -0
  11. package/dist/{chunk-F7UOLKWV.cjs → chunk-D4TOPSHV.cjs} +518 -473
  12. package/dist/chunk-D4TOPSHV.cjs.map +1 -0
  13. package/dist/{chunk-VFTVZJO5.cjs → chunk-EOFWSGLU.cjs} +4 -4
  14. package/dist/{chunk-VFTVZJO5.cjs.map → chunk-EOFWSGLU.cjs.map} +1 -1
  15. package/dist/{chunk-S63IOKFP.js → chunk-MMEQKFFQ.js} +44 -9
  16. package/dist/chunk-MMEQKFFQ.js.map +1 -0
  17. package/dist/{chunk-OXF7VT7S.js → chunk-X5DAXBJI.js} +2 -2
  18. package/dist/{chunk-GOZGBVJP.js → chunk-Z7QQZ53L.js} +2 -2
  19. package/dist/{chunk-GOZGBVJP.js.map → chunk-Z7QQZ53L.js.map} +1 -1
  20. package/dist/{index-DRJqwO2y.d.ts → index-BrBBUb1i.d.ts} +141 -83
  21. package/dist/{index-BWpwZRQw.d.cts → index-DejnxQpK.d.cts} +141 -83
  22. package/dist/index.cjs +547 -127
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +145 -9
  25. package/dist/index.d.ts +145 -9
  26. package/dist/index.js +427 -7
  27. package/dist/index.js.map +1 -1
  28. package/dist/schemas.cjs +5 -3
  29. package/dist/schemas.cjs.map +1 -1
  30. package/dist/schemas.d.cts +2 -2
  31. package/dist/schemas.d.ts +2 -2
  32. package/dist/schemas.js +4 -2
  33. package/dist/scoring.cjs +3 -3
  34. package/dist/scoring.d.cts +33 -5
  35. package/dist/scoring.d.ts +33 -5
  36. package/dist/scoring.js +2 -2
  37. package/dist/xapi.cjs +3 -3
  38. package/dist/xapi.d.cts +1 -1
  39. package/dist/xapi.d.ts +1 -1
  40. package/dist/xapi.js +2 -2
  41. package/package.json +1 -1
  42. package/vectors/README.md +9 -3
  43. package/vectors/scoring.json +1258 -10
  44. package/dist/chunk-6P6XK2LI.cjs.map +0 -1
  45. package/dist/chunk-F7UOLKWV.cjs.map +0 -1
  46. package/dist/chunk-IRUD73IQ.js.map +0 -1
  47. package/dist/chunk-S63IOKFP.js.map +0 -1
  48. /package/dist/{chunk-OXF7VT7S.js.map → chunk-X5DAXBJI.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # @intellectif/lk-core
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 71de78d: The grade return trip now checks its numbers. A returned grade that cannot be a grade — `NaN`, `Infinity`, a negative, 85 "out of 1", any score out of 0 — no longer composes to a final result. **This can change a recorded grade**, for exactly that population, and for nothing else.
8
+
9
+ **What was wrong.** `evaluate` refuses a non-finite score and `gradeFromRubric` refuses one outside `[0,1]`, but the path a deferred grade comes back on took any number on trust. `outcomeFromGrade` mirrored whatever `GradeRecord` it was handed, and `composeAssessmentScore` divided by it, reading a `maxScore` of 0 or less as 1. Composed beside one full-marks item:
10
+
11
+ - a grader that returned raw points (`score: 85, maxScore: 1`) gave a **final** result, score 43, passed;
12
+ - `score: NaN` gave a final `NaN` score — `null` once stored as JSON — beside a section marked passed;
13
+ - `score: Infinity` gave a final pass;
14
+ - `score: 0.5, maxScore: 0` was counted as 0.5 out of 1.
15
+
16
+ **What changes.**
17
+
18
+ - **`outcomeFromGrade`** lifts a record only when `maxScore` is a positive, finite number and `score` a finite number from 0 to it. A score up to one part in a billion above `maxScore` is float noise (`1.0000000000000002` from summing weighted parts) and passes as it is, unclamped. The tolerance is relative, and it is the bound lk-react's grade reader already applies before it shows a grade, so the two packages agree on what a grade is. There is no tolerance below 0: nothing the SDK produces is negative. A record that fails comes back as `{ status: 'deferred', reason: 'grade_rejected', maxScore: 1, rejectedGrade }`. The record is kept verbatim for audit, and nothing on it is mirrored, so its `passed` is never read as a verdict. `hasGrade` is `false` for it. A valid record lifts exactly as before.
19
+ - **`composeAssessmentScore`** applies the same check to every `scored` and `graded` outcome, including ones an earlier release stored. It treats a failing outcome, and a `grade_rejected` one, as still owed a grade: out of the denominator, listed in `pendingSlotIds`, and also in a new **`rejectedSlotIds`** on the result and on its section. The result stays `provisional` with `passed: null`.
20
+ - **`aiExplanationRequest`** returns `null` for a `scored` grade of record that fails the same check. In 0.17.0 it handed the numbers to the model as they were: 85 "out of 1" on a wrong multiple-choice answer went as `category: 'correct'`, passed; `NaN` went as a `null` score. Grading locally instead would speak to a grade the screen does not show, so there is no explanation at all, and lk-react's "Explain my answer" reads "No explanation is available right now." without calling the port.
21
+ - **`gradeFromRubric`** refuses a criterion weight below 0 as `unscorable`, and likewise a weight sum that overflows to Infinity. It checked only that the weights summed to a positive number, so `[0 ×2, 1 ×−1]` came back as a real 0 and `[1 ×2, 0 ×−1]` as a perfect 1, the clamp meant for float noise hiding both. Only criteria that enter the arithmetic are read, as for `score` and `maxScore`. A rubric already refused for its weight sum keeps the reason it always had. `[1 ×1e308, 0 ×1e308]` used to grade 0: its sum is Infinity, and every criterion divided into it.
22
+
23
+ **Why deferred, and not unscorable.** `unscorable` means a grade is never coming. `composeAssessmentScore` leaves such a slot out of the denominator and still lets the attempt go `final` with a verdict. A failing grade on the wrong scale would vanish, and the rest of the paper would be recorded as a pass. `deferred` is what the slot is: a real grade is still owed. Every reader that already treats `deferred` as not yet, lk-react's components included, stays safe without knowing the new reason. `rejectedSlotIds` tells a host to re-grade rather than wait. A rejected slot is also listed in `pendingSlotIds`, so `status` is `provisional` exactly when `pendingSlotIds` is non-empty, the rule it has always followed.
24
+
25
+ **One deliberate reversal.** A `maxScore` of 0 or less used to be read as 1 rather than divided by. A unit test and one grade-stability vector pinned that on purpose, to stop a division by zero. Dividing is not the only alternative: nothing is out of 0, so reading 0.5 against a denominator nobody declared invents a grade. Such an outcome is now rejected like any other score that cannot be a grade. The test is rewritten to say so, and the vector `composeAssessmentScore/item-max-score-zero` is re-frozen.
26
+
27
+ **What stays the same.** Of the 564 vectors in the grade-stability corpus, 563 replay byte for byte against this build, both ESM and CJS, and the one that moves is the reversal above. Every input that was a grade composes exactly as before. `rejectedSlotIds` is present only when something was rejected, so a result without one is identical to what earlier releases returned. 28 new vectors cover each refused kind of number, the accepted float noise up to its exact edge, and the new rubric refusals.
28
+
29
+ **Before you upgrade.** Search stored outcomes for a `score` or `maxScore` that is not a finite number (in JSON, `null`), a `maxScore` of 0 or less, or a `score` below 0 or above its `maxScore`. Re-composing an attempt that holds one now returns `provisional` rather than the final result you recorded. Re-grade those slots. `DeferredReason` gains `'grade_rejected'`, so an exhaustive `switch` over it needs a case. The new fields are optional.
30
+
31
+ lk-react needs no code change. A record `outcomeFromGrade` now refuses renders as "Not graded yet.", like any deferred outcome. With lk-react 17, `<WrittenResponse>` showed such a record's numbers ("Score 8500%. Passed." for 85 out of 1). A `graded` outcome stored by an earlier release is not re-read, and still renders as before.
32
+
33
+ ## 0.17.0
34
+
35
+ ### Minor Changes
36
+
37
+ - 15a3435: The contract for AI help a learner reads — an explanation of a graded answer, and hints before one —
38
+ between an activity and a model the host runs. The SDK never calls a model and holds no key or prompt;
39
+ it builds what the model is given and checks what comes back.
40
+
41
+ - **Facts:** `buildAiFacts(data, response, details)` describes an item as the learner saw it, their
42
+ answer, and the SDK's own verdict part by part. It covers multiple choice (every option), fill in the
43
+ blanks and gap select (each blank or gap in passage order, the passage written with `[1]`, `[2]`…),
44
+ and dictation (the word-by-word alignment). On a redacted item the key is absent, except where a
45
+ server's outcome supplies it.
46
+ - **Requests:**
47
+ - `aiExplanationRequest({ data, response, outcome?, learnerLocale? })` carries the facts and the
48
+ `grade`, with its `category` (`correct`, `partly-correct` or `incorrect`). A scored `outcome` wins
49
+ over grading locally, so an explanation speaks to the grade of record.
50
+ - `aiHintRequest({ data, response, previousHints, learnerLocale? })` carries the key and the answer
51
+ so far, marked part by part. It gives none for a dictation, which has hints of its own, or for a
52
+ redacted item.
53
+ - **Checks:**
54
+ - `readAiTextResult` refuses anything but plain text of up to 2,000 characters (`malformed`,
55
+ `empty`, `too-long`), with control characters removed.
56
+ - `checkAiExplanation` also refuses an explanation stating a verdict other than the SDK's
57
+ (`contradicts-grade`).
58
+ - `checkAiHint` also refuses a hint that contains an answer (`reveals-answer`).
59
+ - **The guard:** `hintRevealsAnswer(facts, text)` finds an answer written out, ignoring case,
60
+ accents and punctuation. An answer of one short word counts only where the hint writes it beside a
61
+ neighbour it has in the passage, so a hint can still say "the verb is…".
62
+ - **Permissions:** `aiSupports(type, feature)`, `aiAllowedByContent(data, feature)`, `aiGradeOf`, and
63
+ the types `AiItemFacts`, `AiExplanationRequest`, `AiHintRequest`, `AiTextResult`, `AiProvenance`,
64
+ `ActivityAiPermissions` and more. Two new interaction kinds: `ai-hint-shown` and
65
+ `ai-explanation-shown`.
66
+
67
+ **The author's switch, and one tightened rule.** Every built-in type now defines an optional
68
+ `ai: { hints?: boolean, explanations?: boolean }`: `false` switches that help off for the item,
69
+ wherever it is delivered, and an author can only switch help off. `redact()` keeps both flags, as
70
+ public, and drops any other key inside `ai`; the strict redacted schemas accept them.
71
+
72
+ A field named `ai` was until now an unknown key the loose schemas kept, whatever it held. A sidecar
73
+ such as `ai: 'generated'`, or a flag that is not a boolean, now fails `validateActivity`: check stored
74
+ content before upgrading.
75
+
76
+ See `docs/ai.md`.
77
+
3
78
  ## 0.16.0
4
79
 
5
80
  ### Minor Changes
package/README.md CHANGED
@@ -23,8 +23,8 @@ npm install @intellectif/lk-core
23
23
  - **Scoring engine** — `score(activityType, data, response)` returning `{ score ∈ [0,1], maxScore: 1, passed, feedback, details }`. Pure, deterministic, 100 % test coverage enforced. It **throws** rather than inventing a number: `DeferredScoringError` for a deferred-graded type (`written-response`), and `RedactedScoringError` when handed a `redact()` projection, which has no answer key. Both are on the documented exam path — use `evaluate()` there, which returns `deferred` / `unscorable` instead of throwing.
24
24
  - **Dictation marking as data** — a dictation is graded by the character-level similarity of the typed text to the transcript, with one `ScoringDetail` per word carrying its own `score`. `alignDictation(data, text)` returns the same comparison the grade was made of (normalised strings, word pairings, which accepted transcript won), `diffDictationChars(reference, attempt)` the character-level edits for a marked display, and `dictationReferenceWords(data)` the ids and words a stored result can be checked against — so a review screen or an analytics job never re-implements the arithmetic. `score()` and `evaluate()` take an optional `{ rounding }` to compare the pass line as displayed.
25
25
  - **`evaluate(data, response)` → `ItemOutcome`** — the resilient result: `scored`, `deferred` (graded later by an AI or a human), `graded` (the grade came back), or `unscorable`. "Not graded yet" is expressible in the type system and is never conflated with a zero.
26
- - **Deferred grading, both directions** — `GradeRecord`, `gradeFromRubric()` (the weighted total as a pure function of the grader's judgements — never the model's arithmetic; declare `maxScore` per criterion if your grader works out of 100 or out of 9), `outcomeFromGrade()`, `hasGrade()`.
27
- - **Assessment composition** — `composeAssessmentScore()` for weighted, sectioned totals, with a `provisional` status so an unmarked essay never deflates a grade, and `RoundingPolicy` / `roundGrade` / `gte` / `classifyBand` keeping grade rounding and band classification as the two distinct operations they are.
26
+ - **Deferred grading, both directions** — `GradeRecord`, `gradeFromRubric()` (the weighted total as a pure function of the grader's judgements — never the model's arithmetic; declare `maxScore` per criterion if your grader works out of 100 or out of 9), `outcomeFromGrade()` (which refuses a record whose numbers cannot be a grade instead of mirroring them), `hasGrade()`.
27
+ - **Assessment composition** — `composeAssessmentScore()` for weighted, sectioned totals, with a `provisional` status so an unmarked essay never deflates a grade — and a grade out of contract (`NaN`, 85 "out of 1") never finalises one — and `RoundingPolicy` / `roundGrade` / `gte` / `classifyBand` keeping grade rounding and band classification as the two distinct operations they are.
28
28
  - **Item groups** — `Stimulus` + `ItemGroup` (one passage or recording serving several questions), `flattenSequence()` for seeded, shuffle-atomic presentation order with stable slot identity. Slot ids are positional by default (`"3"`, `"1.0"`); author a `slotKey` on an entry and its id survives an insert, so a stored response cannot slide onto a different question.
29
29
  - **Attempt plans** — `planAttempt(entries, { seed, shuffleEntries, points })` freezes the exact paper a learner sat: slot order, per-slot points, a content hash of each item, and a `planHash` over the lot. `verifyAttemptPlan(plan, current)` reports the drift when that content is later edited (`changedSlotIds`, `changedStimulusSlotIds`, `changedPointsSlotIds`, `missingSlotIds`, …) — the thing you need during a remark or an appeal. `scoredItemsFromPlan(plan, outcomes)` turns collected outcomes into the `ScoredItem[]` composition wants, and defaults a slot with **no** outcome to `deferred` rather than silently shrinking the denominator.
30
30
  - **Attempt state** — `serializeAttemptState(plan, progress)` / `restoreAttemptState(plan, snapshot)` round-trip an interrupted attempt (responses, submitted slots, position) and are **bound to the plan**: restoring answers onto a paper the learner never sat throws instead of succeeding quietly. `diffResponses(before, after)` reports what moved between two snapshots, including an answer the learner *cleared*.
@@ -63,6 +63,197 @@ 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 contract between an activity and a model the HOST runs, for help a
68
+ * learner reads: an explanation of a graded answer, and hints before one.
69
+ *
70
+ * The SDK never calls a model and holds no key, prompt or provider. It builds
71
+ * the facts a model is given — from the item, the learner's answer and the
72
+ * SDK's own scorer — and checks what comes back. The model explains a verdict
73
+ * the SDK reached; it never reaches one.
74
+ */
75
+ /**
76
+ * What an author allows AI to do for one item. Absent, or a feature left out,
77
+ * leaves the choice to the deployment. `false` switches a feature off for this
78
+ * item wherever it is delivered; an author can only switch a feature off,
79
+ * never force one on.
80
+ *
81
+ * Switch hints off where any hint would give the answer away — a
82
+ * one-word vocabulary item, say.
83
+ */
84
+ interface ActivityAiPermissions {
85
+ /** An explanation of the learner's graded answer. */
86
+ explanations?: boolean;
87
+ /** Hints before the learner submits. */
88
+ hints?: boolean;
89
+ }
90
+ /**
91
+ * An activity as a renderer holds it: full data or a `redact()` projection.
92
+ * Structural rather than the data union, so a renderer's own view of an item —
93
+ * whose answer-key fields may be absent — is accepted as it is. The AI
94
+ * functions read every field defensively.
95
+ */
96
+ interface AiActivityInput {
97
+ readonly type: string;
98
+ readonly id: string;
99
+ readonly title: string;
100
+ readonly locale?: unknown;
101
+ readonly redacted?: unknown;
102
+ readonly ai?: unknown;
103
+ }
104
+ /** The two kinds of help a learner can be given. */
105
+ type AiFeature = 'explanation' | 'hint';
106
+ /** The activity types the SDK builds facts for. */
107
+ type AiSupportedActivityType = 'multiple-choice' | 'fill-in-the-blanks' | 'gap-select' | 'dictation';
108
+ /**
109
+ * A whole answer's grade, from the SDK's scorer. `category` is the verdict an
110
+ * explanation must agree with: all marks, some, or none.
111
+ */
112
+ interface AiGrade {
113
+ score: number;
114
+ maxScore: number;
115
+ passed: boolean;
116
+ category: AiVerdict;
117
+ }
118
+ /** The three verdicts an explanation can speak to. */
119
+ type AiVerdict = 'correct' | 'partly-correct' | 'incorrect';
120
+ interface AiFactsBase {
121
+ activityId: string;
122
+ title: string;
123
+ /** The language the item is written in, when the author gave one. */
124
+ locale?: string;
125
+ }
126
+ /** One option of a multiple-choice question, and what the learner did with it. */
127
+ interface AiOptionFact {
128
+ id: string;
129
+ text: string;
130
+ chosen: boolean;
131
+ /** Whether this option is part of the answer; `null` when the key is not available. */
132
+ correct: boolean | null;
133
+ /** The author's feedback on this option. */
134
+ feedback?: string;
135
+ }
136
+ interface AiMultipleChoiceFacts extends AiFactsBase {
137
+ activityType: 'multiple-choice';
138
+ question: string;
139
+ mode: 'single' | 'multi';
140
+ options: AiOptionFact[];
141
+ }
142
+ /** One blank, in the order it appears in the passage. */
143
+ interface AiBlankFact {
144
+ id: string;
145
+ /** 1-based, in passage order. */
146
+ position: number;
147
+ /** What the learner typed, or `''`. */
148
+ typed: string;
149
+ /** The answers that count; `null` when the key is not available. */
150
+ accepted: string[] | null;
151
+ /** The SDK's verdict on this blank; `null` before it is graded or without a key. */
152
+ correct: boolean | null;
153
+ /** The author's hint for this blank. */
154
+ hint?: string;
155
+ /** The author's feedback for this blank. */
156
+ feedback?: string;
157
+ }
158
+ interface AiFillInTheBlanksFacts extends AiFactsBase {
159
+ activityType: 'fill-in-the-blanks';
160
+ /** The passage, each blank written as `[1]`, `[2]`… in passage order. */
161
+ passage: string;
162
+ blanks: AiBlankFact[];
163
+ }
164
+ /** One gap, in the order it appears in the passage. */
165
+ interface AiGapFact {
166
+ id: string;
167
+ /** 1-based, in passage order. */
168
+ position: number;
169
+ /** The words offered at this gap. */
170
+ choices: string[];
171
+ /** The word the learner chose, or `null`. */
172
+ chosen: string | null;
173
+ /** The right word; `null` when the key is not available. */
174
+ answer: string | null;
175
+ /** The SDK's verdict on this gap; `null` before it is graded or without a key. */
176
+ correct: boolean | null;
177
+ /** The author's feedback for this gap. */
178
+ feedback?: string;
179
+ }
180
+ interface AiGapSelectFacts extends AiFactsBase {
181
+ activityType: 'gap-select';
182
+ /** The passage, each gap written as `[1]`, `[2]`… in passage order. */
183
+ passage: string;
184
+ gaps: AiGapFact[];
185
+ }
186
+ /** One word of a dictation, as the SDK's alignment paired it. */
187
+ interface AiWordFact {
188
+ /** The word the recording says; `''` for an extra word the learner added. */
189
+ expected: string;
190
+ /** The word the learner typed; `''` for a word they left out. */
191
+ typed: string;
192
+ status: 'correct' | 'incorrect' | 'missing' | 'extra';
193
+ }
194
+ interface AiDictationFacts extends AiFactsBase {
195
+ activityType: 'dictation';
196
+ /** What the recording says; `null` when the key is not available. */
197
+ transcript: string | null;
198
+ /** What the learner typed. */
199
+ typed: string;
200
+ /** The alignment word by word; `null` when the key is not available. */
201
+ words: AiWordFact[] | null;
202
+ }
203
+ /**
204
+ * What a model is told about one item and one answer: the question as the
205
+ * learner saw it, what they answered, and — part by part — the SDK's verdict.
206
+ */
207
+ type AiItemFacts = AiMultipleChoiceFacts | AiFillInTheBlanksFacts | AiGapSelectFacts | AiDictationFacts;
208
+ /**
209
+ * Asks for an explanation of a graded answer. Sent after the SDK has graded
210
+ * it, so `grade` is always present, and an explanation that contradicts
211
+ * `grade.category` is refused.
212
+ */
213
+ interface AiExplanationRequest {
214
+ feature: 'explanation';
215
+ facts: AiItemFacts;
216
+ grade: AiGrade;
217
+ /** The language to answer in: the learner's interface language, when known. */
218
+ learnerLocale?: string;
219
+ }
220
+ /**
221
+ * Asks for the next hint before the learner submits. `facts` carries the key
222
+ * so a model can hint toward the answer; the SDK refuses any hint that
223
+ * contains it.
224
+ */
225
+ interface AiHintRequest {
226
+ feature: 'hint';
227
+ facts: AiItemFacts;
228
+ /** 1-based: the hint being asked for. */
229
+ hintNumber: number;
230
+ /** The hints already shown for this item, oldest first. */
231
+ previousHints: string[];
232
+ learnerLocale?: string;
233
+ }
234
+ /** What produced an AI text, for the record a host keeps. */
235
+ interface AiProvenance {
236
+ /** The model's identifier, e.g. a name and version. */
237
+ model?: string;
238
+ /** A hash or id of the prompt revision. */
239
+ promptHash?: string;
240
+ /** ISO 8601. */
241
+ generatedAt?: string;
242
+ }
243
+ /**
244
+ * What a host's port returns. Plain text: the SDK renders it as text, never
245
+ * as HTML. An explanation may state the verdict it assumed; one that
246
+ * disagrees with the SDK's is refused.
247
+ */
248
+ interface AiTextResult {
249
+ text: string;
250
+ /** For an explanation: the verdict the model explained. */
251
+ verdict?: AiVerdict;
252
+ provenance?: AiProvenance;
253
+ }
254
+ /** Why a result was not shown. */
255
+ type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
256
+
66
257
  /**
67
258
  * The return trip for deferred grading.
68
259
  *
@@ -139,7 +330,11 @@ interface CriterionScore {
139
330
  band?: string;
140
331
  /** The grader's comment for this criterion, addressed to the learner. */
141
332
  comment?: string;
142
- /** Weight actually applied, echoed from the rubric so the total is checkable. */
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
+ */
143
338
  weight?: number;
144
339
  /** Not applicable to this submission (e.g. interaction on a monologue task). */
145
340
  notApplicable?: boolean;
@@ -165,6 +360,10 @@ interface InlineCorrection {
165
360
  * that works in points does not have to normalise by hand: declare
166
361
  * `maxScore` on each {@link CriterionScore} and let `gradeFromRubric` do the
167
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.
168
367
  */
169
368
  interface GradeRecord {
170
369
  score: number;
@@ -590,6 +789,8 @@ interface MultipleChoiceData {
590
789
  learningObjectives?: string[];
591
790
  /** Subjective difficulty on a 1–5 scale. */
592
791
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
792
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
793
+ ai?: ActivityAiPermissions;
593
794
  }
594
795
  /** Configuration for a single fill-in-the-blank slot. */
595
796
  interface BlankConfig {
@@ -724,6 +925,8 @@ interface GapSelectData {
724
925
  learningObjectives?: string[];
725
926
  /** Subjective difficulty on a 1–5 scale. */
726
927
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
928
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
929
+ ai?: ActivityAiPermissions;
727
930
  }
728
931
  interface FillInTheBlanksData {
729
932
  schemaVersion: '1.0';
@@ -756,6 +959,8 @@ interface FillInTheBlanksData {
756
959
  learningObjectives?: string[];
757
960
  /** Subjective difficulty on a 1–5 scale. */
758
961
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
962
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
963
+ ai?: ActivityAiPermissions;
759
964
  }
760
965
  /** A single criterion within a written-response grading rubric. */
761
966
  interface WrittenResponseRubricCriterion {
@@ -818,6 +1023,8 @@ interface WrittenResponseData {
818
1023
  learningObjectives?: string[];
819
1024
  /** Subjective difficulty on a 1–5 scale. */
820
1025
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1026
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1027
+ ai?: ActivityAiPermissions;
821
1028
  }
822
1029
  /**
823
1030
  * A second, slower recording of a dictation — a separate file (a text-to-speech
@@ -920,6 +1127,8 @@ interface DictationData {
920
1127
  learningObjectives?: string[];
921
1128
  /** Subjective difficulty on a 1–5 scale. */
922
1129
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1130
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1131
+ ai?: ActivityAiPermissions;
923
1132
  }
924
1133
  /** A dimension of speech a pronunciation assessor scores, on a 0–100 scale. */
925
1134
  type ReadAloudDimension = 'accuracy' | 'fluency' | 'completeness' | 'prosody';
@@ -1010,6 +1219,8 @@ interface ReadAloudData {
1010
1219
  learningObjectives?: string[];
1011
1220
  /** Subjective difficulty on a 1–5 scale. */
1012
1221
  difficultyLevel?: 1 | 2 | 3 | 4 | 5;
1222
+ /** What an author allows AI to do for this item: see {@link ActivityAiPermissions}. */
1223
+ ai?: ActivityAiPermissions;
1013
1224
  }
1014
1225
  /**
1015
1226
  * Maps each ActivityType string to its learner-response shape. Open for
@@ -1185,8 +1396,13 @@ interface DeferredScoringPartial {
1185
1396
  * a grade has not landed, or the attempt is still open has earned nothing
1186
1397
  * yet, and recording it as zero is how an incomplete attempt becomes a
1187
1398
  * plausible-looking fail.
1399
+ * - `grade_rejected` — a grade came back, and `outcomeFromGrade` refused it
1400
+ * because its numbers cannot be a grade (a `score` or `maxScore` that is not
1401
+ * a finite number, a `maxScore` of 0 or less, or a score outside 0 to
1402
+ * `maxScore`). The record is kept on `rejectedGrade`. A real grade is still
1403
+ * owed, so re-grade the slot: waiting will not bring one.
1188
1404
  */
1189
- type DeferredReason = 'requires_async_grading' | 'no_response_recorded';
1405
+ type DeferredReason = 'requires_async_grading' | 'no_response_recorded' | 'grade_rejected';
1190
1406
  /**
1191
1407
  * The outcome of evaluating a learner response against an activity — the
1192
1408
  * union `evaluate()` returns. Unlike {@link ScoringResult}, it can express
@@ -1213,6 +1429,12 @@ type ItemOutcome = {
1213
1429
  maxScore: number;
1214
1430
  /** Synchronously computable progress facts (word bounds, counts). */
1215
1431
  partial?: DeferredScoringPartial;
1432
+ /**
1433
+ * The record `outcomeFromGrade` refused, verbatim, when `reason` is
1434
+ * `grade_rejected` — kept for audit and for the re-grade. Nothing on it
1435
+ * is mirrored onto the outcome, and nothing reads it as a grade.
1436
+ */
1437
+ rejectedGrade?: GradeRecord;
1216
1438
  } | {
1217
1439
  status: 'graded';
1218
1440
  /**
@@ -1320,7 +1542,15 @@ type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled'
1320
1542
  * primary line's language, or `null` with captions off; `payload.secondary` is
1321
1543
  * the second line's, or `null` with none showing. Fires when either changes.
1322
1544
  */
1323
- | 'video-captions-changed' | 'video-fullscreen-changed' | 'video-pip-changed' | (string & {});
1545
+ | 'video-captions-changed' | 'video-fullscreen-changed' | 'video-pip-changed'
1546
+ /**
1547
+ * An AI hint was shown to the learner (`hintNumber`, and the port's
1548
+ * `provenance` when it sent one). Only a hint the SDK accepted: a refused one
1549
+ * is never shown and never reported here.
1550
+ */
1551
+ | 'ai-hint-shown'
1552
+ /** An AI explanation of the graded answer was shown (`provenance` when sent). */
1553
+ | 'ai-explanation-shown' | (string & {});
1324
1554
  /** Fired by activity components on every discrete learner interaction. */
1325
1555
  interface InteractionEvent {
1326
1556
  /** The kind of interaction that occurred. */
@@ -1333,4 +1563,4 @@ interface InteractionEvent {
1333
1563
  payload: Record<string, unknown>;
1334
1564
  }
1335
1565
 
1336
- export { type ValidationResult as $, type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredReason as D, type MultipleChoiceData as E, type FillInTheBlanksData as F, type GapSelectBank as G, type MultipleChoiceLearnerResponse as H, type InlineCorrection as I, type MultipleChoiceOption as J, type MultipleChoiceOptionMedia as K, type LearnerResponse as L, type MediaPlaybackPolicy as M, type NativeControlHint as N, type ReadAloudDimension as O, type ReadAloudDimensionWeight as P, type ReadAloudLearnerResponse as Q, type ReadAloudData as R, type ReadAloudSlowMedia as S, type RecordingBounds as T, type RecordingRef as U, type ScoringDetail as V, type ScoringOutcome as W, type ScoringResult as X, type TextMatchPolicy as Y, type TextMatchResult as Z, type ValidationError as _, type ActivityDataMap as a, type WrittenResponseData as a0, type WrittenResponseLearnerResponse as a1, type WrittenResponseRubric as a2, type WrittenResponseRubricCriterion as a3, type XAPIActor as a4, type XAPIConfig as a5, type XAPIContext as a6, type XAPIContextActivities as a7, type XAPIError as a8, type XAPIObject as a9, type XAPIResult as aa, type XAPIScore as ab, type XAPIStatement as ac, type XAPIVerbObject as ad, levenshteinDistance as ae, matchText as af, type ActivityFeedback as b, type ActivityMedia as c, type ActivityResult as d, type ActivityType as e, type DeferredScoringPartial as f, type DictationData as g, type DictationEquivalence as h, type DictationLearnerResponse as i, type DictationSlowMedia as j, type DictationTolerance as k, type FillInTheBlanksLearnerResponse as l, type GapSelectChoice as m, type GapSelectData as n, type GapSelectGap as o, type GapSelectLearnerResponse as p, type GradeRecord as q, type Grader as r, type GraderKind as s, type GraderUsage as t, type GradingState as u, type InteractionEvent as v, type InteractionKind as w, type ItemOutcome as x, type LearnerResponseMap as y, type MediaTrack as z };
1566
+ export { type MediaPlaybackPolicy as $, type ActivityAiPermissions as A, type BlankConfig as B, type CriterionScore as C, type DeferredReason as D, type DictationData as E, type DictationEquivalence as F, type DictationLearnerResponse as G, type DictationSlowMedia as H, type DictationTolerance as I, type FillInTheBlanksData as J, type FillInTheBlanksLearnerResponse as K, type GapSelectBank as L, type GapSelectChoice as M, type GapSelectData as N, type GapSelectGap as O, type GapSelectLearnerResponse as P, type GradeRecord as Q, type Grader as R, type GraderKind as S, type GraderUsage as T, type GradingState as U, type InlineCorrection as V, type InteractionEvent as W, type InteractionKind as X, type ItemOutcome as Y, type LearnerResponse as Z, type LearnerResponseMap as _, type ActivityData as a, type MediaTrack as a0, type MultipleChoiceData as a1, type MultipleChoiceLearnerResponse as a2, type MultipleChoiceOption as a3, type MultipleChoiceOptionMedia as a4, type NativeControlHint as a5, type ReadAloudData as a6, type ReadAloudDimension as a7, type ReadAloudDimensionWeight as a8, type ReadAloudLearnerResponse as a9, type ReadAloudSlowMedia as aa, type RecordingBounds as ab, type RecordingRef as ac, type ScoringDetail as ad, type ScoringOutcome as ae, type ScoringResult as af, type TextMatchPolicy as ag, type TextMatchResult as ah, type ValidationError as ai, type ValidationResult as aj, type WrittenResponseData as ak, type WrittenResponseLearnerResponse as al, type WrittenResponseRubric as am, type WrittenResponseRubricCriterion as an, type XAPIActor as ao, type XAPIConfig as ap, type XAPIContext as aq, type XAPIContextActivities as ar, type XAPIError as as, type XAPIObject as at, type XAPIResult as au, type XAPIScore as av, type XAPIStatement as aw, type XAPIVerbObject as ax, levenshteinDistance as ay, matchText 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 AiDictationFacts as i, type AiExplanationRequest as j, type AiFeature as k, type AiFillInTheBlanksFacts as l, type AiGapFact as m, type AiGapSelectFacts as n, type AiGrade as o, type AiHintRequest as p, type AiItemFacts as q, type AiMultipleChoiceFacts as r, type AiOptionFact as s, type AiProvenance as t, type AiRefusal as u, type AiSupportedActivityType as v, type AiTextResult as w, type AiVerdict as x, type AiWordFact as y, type DeferredScoringPartial as z };