@intellectif/lk-core 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +3 -2
- package/dist/{activity-BmjBjXVJ.d.cts → activity-f0bgZSTP.d.cts} +175 -134
- package/dist/{activity-BmjBjXVJ.d.ts → activity-f0bgZSTP.d.ts} +175 -134
- package/dist/ai-check.cjs +283 -0
- package/dist/ai-check.cjs.map +1 -0
- package/dist/ai-check.d.cts +106 -0
- package/dist/ai-check.d.ts +106 -0
- package/dist/ai-check.js +283 -0
- package/dist/ai-check.js.map +1 -0
- package/dist/chunk-BIJCO2HM.js +443 -0
- package/dist/chunk-BIJCO2HM.js.map +1 -0
- package/dist/{chunk-XNMKDFPW.cjs → chunk-COBK2RUX.cjs} +44 -9
- package/dist/chunk-COBK2RUX.cjs.map +1 -0
- package/dist/chunk-FVZJ4X3Y.cjs +443 -0
- package/dist/chunk-FVZJ4X3Y.cjs.map +1 -0
- package/dist/{chunk-WXS5EB65.js → chunk-MMEQKFFQ.js} +43 -8
- package/dist/chunk-MMEQKFFQ.js.map +1 -0
- package/dist/{index-G_KusUcK.d.ts → index-DMCM3e3P.d.ts} +1 -1
- package/dist/{index-CvfeD_cU.d.cts → index-bpuDhvXV.d.cts} +1 -1
- package/dist/index.cjs +37 -422
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -11
- package/dist/index.d.ts +30 -11
- package/dist/index.js +37 -422
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/scoring.cjs +2 -2
- package/dist/scoring.d.cts +33 -5
- package/dist/scoring.d.ts +33 -5
- package/dist/scoring.js +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/package.json +11 -1
- package/vectors/README.md +9 -3
- package/vectors/scoring.json +1258 -10
- package/dist/chunk-WXS5EB65.js.map +0 -1
- package/dist/chunk-XNMKDFPW.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# @intellectif/lk-core
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d576382: Groundwork under the AI line: what a call cost, what was refused, and a way to test a prompt.
|
|
8
|
+
|
|
9
|
+
- **`AiTextResult.usage`** — what the call cost, as your provider reported it to your port. It is the
|
|
10
|
+
same `GraderUsage` a returned grade already carries (`promptTokens`, `completionTokens`, `costUsd`),
|
|
11
|
+
so one shape covers help for a learner and marking by a grader. The SDK never estimates it and never
|
|
12
|
+
adds it up; it carries what you send, and drops a number that is not finite and zero or more rather
|
|
13
|
+
than let it into your totals.
|
|
14
|
+
- **`ai-help-refused`** joins the interaction kinds: a host can now see, in production, that its model
|
|
15
|
+
wrote a hint containing the answer or an explanation of a verdict the SDK did not reach. The
|
|
16
|
+
development console warning said it only to a developer. The event carries the feature and the
|
|
17
|
+
reason and **never the text** — the text of a refused hint is the answer.
|
|
18
|
+
- **`@intellectif/lk-core/ai-check`, a new entry point: a test kit for your prompts.** `runAiCheck`
|
|
19
|
+
makes the calls a learner's questions would make — on items whose answers the SDK knows — and runs
|
|
20
|
+
the same checks it runs before a learner sees anything, reporting what would have been shown and
|
|
21
|
+
what would have been refused, with the reason. `aiCheckCases` hands over the cases (every type it
|
|
22
|
+
explains, answered right, wrong and partly right; every type it hints for, before an answer and
|
|
23
|
+
after a wrong one), and takes cases of your own built on your own items. `formatAiCheckReport`
|
|
24
|
+
prints a run for a CI log.
|
|
25
|
+
|
|
26
|
+
It is a separate entry point because it is for your test run, not a learner's browser: nothing a
|
|
27
|
+
page renders imports it.
|
|
28
|
+
|
|
29
|
+
The empty `@intellectif/lk-ai` placeholder has been deleted — it never held code, and the ports it
|
|
30
|
+
was planned for live here, beside the scorers whose readings the checks are.
|
|
31
|
+
|
|
32
|
+
See `docs/ai.md`.
|
|
33
|
+
|
|
34
|
+
## 0.18.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
40
|
+
**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:
|
|
41
|
+
|
|
42
|
+
- a grader that returned raw points (`score: 85, maxScore: 1`) gave a **final** result, score 43, passed;
|
|
43
|
+
- `score: NaN` gave a final `NaN` score — `null` once stored as JSON — beside a section marked passed;
|
|
44
|
+
- `score: Infinity` gave a final pass;
|
|
45
|
+
- `score: 0.5, maxScore: 0` was counted as 0.5 out of 1.
|
|
46
|
+
|
|
47
|
+
**What changes.**
|
|
48
|
+
|
|
49
|
+
- **`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.
|
|
50
|
+
- **`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`.
|
|
51
|
+
- **`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.
|
|
52
|
+
- **`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.
|
|
53
|
+
|
|
54
|
+
**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.
|
|
55
|
+
|
|
56
|
+
**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.
|
|
57
|
+
|
|
58
|
+
**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.
|
|
59
|
+
|
|
60
|
+
**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.
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
3
64
|
## 0.17.0
|
|
4
65
|
|
|
5
66
|
### 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()
|
|
27
|
-
- **Assessment composition** — `composeAssessmentScore()` for weighted, sectioned totals, with a `provisional` status so an unmarked essay never deflates a grade
|
|
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*.
|
|
@@ -115,6 +115,7 @@ result.passed; // boolean, or null while provisional. Never record a provision
|
|
|
115
115
|
| `@intellectif/lk-core/schemas` | Zod schemas + `validateActivity` + JSON Schema export |
|
|
116
116
|
| `@intellectif/lk-core/scoring` | `score`, `evaluate`, `composeAssessmentScore`, rounding, text matching |
|
|
117
117
|
| `@intellectif/lk-core/xapi` | `xAPIBuilder`, `XAPIVerb`, `validateXAPIStatement` |
|
|
118
|
+
| `@intellectif/lk-core/ai-check` | `runAiCheck`, `aiCheckCases`, `formatAiCheckReport` — test your AI prompts in CI |
|
|
118
119
|
|
|
119
120
|
The barrel re-exports everything, and it is the **only** entry point for attempt
|
|
120
121
|
plans, attempt state, item groups, redaction, the type registry, authoring drafts
|
|
@@ -63,6 +63,147 @@ declare function levenshteinDistance(a: string, b: string, max: number): number;
|
|
|
63
63
|
*/
|
|
64
64
|
declare function matchText(input: string, accepted: string | readonly string[], policy?: TextMatchPolicy): TextMatchResult;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The return trip for deferred grading.
|
|
68
|
+
*
|
|
69
|
+
* `evaluate()` can say a submission is `deferred` — graded later by an AI or a
|
|
70
|
+
* human — but until now there was no type for the grade that comes BACK, so
|
|
71
|
+
* every consumer invented one. The shapes here are the intersection of two
|
|
72
|
+
* independent production graders (a rubric-based essay grader and a CEFR
|
|
73
|
+
* speaking grader) that converged on the same envelope: a normalised total,
|
|
74
|
+
* per-criterion scores with comments, grader-authored artefacts, a confidence
|
|
75
|
+
* signal, an explicit human-review flag, and provenance.
|
|
76
|
+
*
|
|
77
|
+
* The SDK models the SHAPE and the arithmetic. It never calls a model, never
|
|
78
|
+
* holds a key, and never decides what a rubric means — that stays yours.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lifecycle of a deferred grade. A grade exists only in the `graded` state;
|
|
83
|
+
* every other state means "no grade, and here is why" — which is precisely the
|
|
84
|
+
* distinction that stops an ungraded submission being rendered as a zero.
|
|
85
|
+
*/
|
|
86
|
+
type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
|
|
87
|
+
/**
|
|
88
|
+
* Who produced a grade.
|
|
89
|
+
*
|
|
90
|
+
* - `'auto'` — a non-generative measurement engine, for example a
|
|
91
|
+
* pronunciation-assessment service.
|
|
92
|
+
* - `'ai'` — a generative model.
|
|
93
|
+
* - `'human'` — a person.
|
|
94
|
+
*/
|
|
95
|
+
type GraderKind = 'ai' | 'human' | 'auto';
|
|
96
|
+
/** Provenance of a grade, so a re-grade two years later is explicable. */
|
|
97
|
+
interface Grader {
|
|
98
|
+
kind: GraderKind;
|
|
99
|
+
/** Identifier of the human grader, when `kind` is `'human'`. */
|
|
100
|
+
id?: string;
|
|
101
|
+
/** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
|
|
102
|
+
model?: string;
|
|
103
|
+
/** Hash or id of the prompt/rubric revision used, for auditability. */
|
|
104
|
+
promptHash?: string;
|
|
105
|
+
}
|
|
106
|
+
/** Optional cost/usage telemetry emitted by an AI grader. */
|
|
107
|
+
interface GraderUsage {
|
|
108
|
+
promptTokens?: number;
|
|
109
|
+
completionTokens?: number;
|
|
110
|
+
costUsd?: number;
|
|
111
|
+
}
|
|
112
|
+
/** A grader's verdict on one rubric criterion. */
|
|
113
|
+
interface CriterionScore {
|
|
114
|
+
/** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
|
|
115
|
+
name: string;
|
|
116
|
+
/**
|
|
117
|
+
* The grader's score for this criterion, out of {@link CriterionScore.maxScore}
|
|
118
|
+
* (which defaults to 1, the SDK's scaled convention). Absent when the
|
|
119
|
+
* criterion is judged on an ordinal `band`.
|
|
120
|
+
*/
|
|
121
|
+
score?: number;
|
|
122
|
+
/**
|
|
123
|
+
* What `score` is out of. Defaults to `1`.
|
|
124
|
+
*
|
|
125
|
+
* Real graders rarely work in [0,1]: rubrics come back out of 100, out of 9
|
|
126
|
+
* (CEFR/IELTS bands), or out of a points total that differs between
|
|
127
|
+
* criteria. Before this field the only way to use {@link gradeFromRubric}
|
|
128
|
+
* was to normalise by hand first — and a grader that skipped that step got
|
|
129
|
+
* its scores REJECTED, which pushed integrators back to letting the model
|
|
130
|
+
* compute the weighted total itself. That is precisely the arithmetic this
|
|
131
|
+
* SDK exists to take away from a language model.
|
|
132
|
+
*
|
|
133
|
+
* Set it per criterion, so a rubric mixing a /100 criterion with a /9 band
|
|
134
|
+
* score still composes. `gradeFromRubric` normalises `score / maxScore`
|
|
135
|
+
* before weighting; the resulting ratio must land in [0,1].
|
|
136
|
+
*/
|
|
137
|
+
maxScore?: number;
|
|
138
|
+
/** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
|
|
139
|
+
band?: string;
|
|
140
|
+
/** The grader's comment for this criterion, addressed to the learner. */
|
|
141
|
+
comment?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Weight actually applied, echoed from the rubric so the total is checkable.
|
|
144
|
+
* Defaults to `1`. Zero or more: `gradeFromRubric` refuses a negative weight
|
|
145
|
+
* rather than let it carry the total outside [0,1].
|
|
146
|
+
*/
|
|
147
|
+
weight?: number;
|
|
148
|
+
/** Not applicable to this submission (e.g. interaction on a monologue task). */
|
|
149
|
+
notApplicable?: boolean;
|
|
150
|
+
}
|
|
151
|
+
/** A suggested correction anchored in the learner's own text. */
|
|
152
|
+
interface InlineCorrection {
|
|
153
|
+
/** Verbatim span from the submission. */
|
|
154
|
+
original: string;
|
|
155
|
+
/** Suggested replacement. */
|
|
156
|
+
corrected: string;
|
|
157
|
+
explanation?: string;
|
|
158
|
+
/** Character offsets into the submitted text, so a UI can anchor it inline. */
|
|
159
|
+
range?: {
|
|
160
|
+
start: number;
|
|
161
|
+
end: number;
|
|
162
|
+
};
|
|
163
|
+
/** Error-type tag (`"article"`, `"tense"`, `"register"`). */
|
|
164
|
+
category?: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A grade that came back from an asynchronous grader. `score` is scaled
|
|
168
|
+
* [0,1] against `maxScore`, matching every other score in the SDK. A grader
|
|
169
|
+
* that works in points does not have to normalise by hand: declare
|
|
170
|
+
* `maxScore` on each {@link CriterionScore} and let `gradeFromRubric` do the
|
|
171
|
+
* arithmetic, which is the whole point of handing it the judgements.
|
|
172
|
+
*
|
|
173
|
+
* `maxScore` is a positive, finite number and `score` a finite number from 0
|
|
174
|
+
* to it. `outcomeFromGrade` refuses a record that is not, rather than mirror
|
|
175
|
+
* numbers nobody can defend onto an outcome.
|
|
176
|
+
*/
|
|
177
|
+
interface GradeRecord {
|
|
178
|
+
score: number;
|
|
179
|
+
maxScore: number;
|
|
180
|
+
passed: boolean;
|
|
181
|
+
/** Narrative feedback addressed to the learner. */
|
|
182
|
+
feedback: string | null;
|
|
183
|
+
/** Per-criterion breakdown, when the grader worked against a rubric. */
|
|
184
|
+
criteria?: CriterionScore[];
|
|
185
|
+
/**
|
|
186
|
+
* Per-item marks carried by a grade — for example the per-word marks of a
|
|
187
|
+
* read-aloud grade. `outcomeFromGrade` does not mirror them onto the
|
|
188
|
+
* outcome; read `outcome.grade.details`.
|
|
189
|
+
*/
|
|
190
|
+
details?: ScoringDetail[];
|
|
191
|
+
/** Corrections anchored in the learner's text. */
|
|
192
|
+
corrections?: InlineCorrection[];
|
|
193
|
+
/** Supporting observations the grader cited. */
|
|
194
|
+
evidence?: string[];
|
|
195
|
+
/** How the grader reached this verdict. */
|
|
196
|
+
rationale?: string;
|
|
197
|
+
/** The grader's own confidence — a signal for routing to human review. */
|
|
198
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
199
|
+
/** The grader is unsure or the submission is atypical; route to a human. */
|
|
200
|
+
requiresHumanReview?: boolean;
|
|
201
|
+
grader?: Grader;
|
|
202
|
+
usage?: GraderUsage;
|
|
203
|
+
/** ISO 8601 timestamp of when the grade was produced. */
|
|
204
|
+
gradedAt?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
66
207
|
/**
|
|
67
208
|
* The contract between an activity and a model the HOST runs, for help a
|
|
68
209
|
* learner reads: an explanation of a graded answer, and hints before one.
|
|
@@ -72,6 +213,7 @@ declare function matchText(input: string, accepted: string | readonly string[],
|
|
|
72
213
|
* SDK's own scorer — and checks what comes back. The model explains a verdict
|
|
73
214
|
* the SDK reached; it never reaches one.
|
|
74
215
|
*/
|
|
216
|
+
|
|
75
217
|
/**
|
|
76
218
|
* What an author allows AI to do for one item. Absent, or a feature left out,
|
|
77
219
|
* leaves the choice to the deployment. `false` switches a feature off for this
|
|
@@ -250,142 +392,20 @@ interface AiTextResult {
|
|
|
250
392
|
/** For an explanation: the verdict the model explained. */
|
|
251
393
|
verdict?: AiVerdict;
|
|
252
394
|
provenance?: AiProvenance;
|
|
253
|
-
}
|
|
254
|
-
/** Why a result was not shown. */
|
|
255
|
-
type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* The return trip for deferred grading.
|
|
259
|
-
*
|
|
260
|
-
* `evaluate()` can say a submission is `deferred` — graded later by an AI or a
|
|
261
|
-
* human — but until now there was no type for the grade that comes BACK, so
|
|
262
|
-
* every consumer invented one. The shapes here are the intersection of two
|
|
263
|
-
* independent production graders (a rubric-based essay grader and a CEFR
|
|
264
|
-
* speaking grader) that converged on the same envelope: a normalised total,
|
|
265
|
-
* per-criterion scores with comments, grader-authored artefacts, a confidence
|
|
266
|
-
* signal, an explicit human-review flag, and provenance.
|
|
267
|
-
*
|
|
268
|
-
* The SDK models the SHAPE and the arithmetic. It never calls a model, never
|
|
269
|
-
* holds a key, and never decides what a rubric means — that stays yours.
|
|
270
|
-
*/
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Lifecycle of a deferred grade. A grade exists only in the `graded` state;
|
|
274
|
-
* every other state means "no grade, and here is why" — which is precisely the
|
|
275
|
-
* distinction that stops an ungraded submission being rendered as a zero.
|
|
276
|
-
*/
|
|
277
|
-
type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
|
|
278
|
-
/**
|
|
279
|
-
* Who produced a grade.
|
|
280
|
-
*
|
|
281
|
-
* - `'auto'` — a non-generative measurement engine, for example a
|
|
282
|
-
* pronunciation-assessment service.
|
|
283
|
-
* - `'ai'` — a generative model.
|
|
284
|
-
* - `'human'` — a person.
|
|
285
|
-
*/
|
|
286
|
-
type GraderKind = 'ai' | 'human' | 'auto';
|
|
287
|
-
/** Provenance of a grade, so a re-grade two years later is explicable. */
|
|
288
|
-
interface Grader {
|
|
289
|
-
kind: GraderKind;
|
|
290
|
-
/** Identifier of the human grader, when `kind` is `'human'`. */
|
|
291
|
-
id?: string;
|
|
292
|
-
/** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
|
|
293
|
-
model?: string;
|
|
294
|
-
/** Hash or id of the prompt/rubric revision used, for auditability. */
|
|
295
|
-
promptHash?: string;
|
|
296
|
-
}
|
|
297
|
-
/** Optional cost/usage telemetry emitted by an AI grader. */
|
|
298
|
-
interface GraderUsage {
|
|
299
|
-
promptTokens?: number;
|
|
300
|
-
completionTokens?: number;
|
|
301
|
-
costUsd?: number;
|
|
302
|
-
}
|
|
303
|
-
/** A grader's verdict on one rubric criterion. */
|
|
304
|
-
interface CriterionScore {
|
|
305
|
-
/** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
|
|
306
|
-
name: string;
|
|
307
395
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*/
|
|
312
|
-
score?: number;
|
|
313
|
-
/**
|
|
314
|
-
* What `score` is out of. Defaults to `1`.
|
|
315
|
-
*
|
|
316
|
-
* Real graders rarely work in [0,1]: rubrics come back out of 100, out of 9
|
|
317
|
-
* (CEFR/IELTS bands), or out of a points total that differs between
|
|
318
|
-
* criteria. Before this field the only way to use {@link gradeFromRubric}
|
|
319
|
-
* was to normalise by hand first — and a grader that skipped that step got
|
|
320
|
-
* its scores REJECTED, which pushed integrators back to letting the model
|
|
321
|
-
* compute the weighted total itself. That is precisely the arithmetic this
|
|
322
|
-
* SDK exists to take away from a language model.
|
|
396
|
+
* What the call cost, as the provider reported it to your port: the same
|
|
397
|
+
* {@link GraderUsage} a returned grade carries, so one shape covers help for
|
|
398
|
+
* a learner and marking by a grader.
|
|
323
399
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
|
|
328
|
-
maxScore?: number;
|
|
329
|
-
/** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
|
|
330
|
-
band?: string;
|
|
331
|
-
/** The grader's comment for this criterion, addressed to the learner. */
|
|
332
|
-
comment?: string;
|
|
333
|
-
/** Weight actually applied, echoed from the rubric so the total is checkable. */
|
|
334
|
-
weight?: number;
|
|
335
|
-
/** Not applicable to this submission (e.g. interaction on a monologue task). */
|
|
336
|
-
notApplicable?: boolean;
|
|
337
|
-
}
|
|
338
|
-
/** A suggested correction anchored in the learner's own text. */
|
|
339
|
-
interface InlineCorrection {
|
|
340
|
-
/** Verbatim span from the submission. */
|
|
341
|
-
original: string;
|
|
342
|
-
/** Suggested replacement. */
|
|
343
|
-
corrected: string;
|
|
344
|
-
explanation?: string;
|
|
345
|
-
/** Character offsets into the submitted text, so a UI can anchor it inline. */
|
|
346
|
-
range?: {
|
|
347
|
-
start: number;
|
|
348
|
-
end: number;
|
|
349
|
-
};
|
|
350
|
-
/** Error-type tag (`"article"`, `"tense"`, `"register"`). */
|
|
351
|
-
category?: string;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* A grade that came back from an asynchronous grader. `score` is scaled
|
|
355
|
-
* [0,1] against `maxScore`, matching every other score in the SDK. A grader
|
|
356
|
-
* that works in points does not have to normalise by hand: declare
|
|
357
|
-
* `maxScore` on each {@link CriterionScore} and let `gradeFromRubric` do the
|
|
358
|
-
* arithmetic, which is the whole point of handing it the judgements.
|
|
359
|
-
*/
|
|
360
|
-
interface GradeRecord {
|
|
361
|
-
score: number;
|
|
362
|
-
maxScore: number;
|
|
363
|
-
passed: boolean;
|
|
364
|
-
/** Narrative feedback addressed to the learner. */
|
|
365
|
-
feedback: string | null;
|
|
366
|
-
/** Per-criterion breakdown, when the grader worked against a rubric. */
|
|
367
|
-
criteria?: CriterionScore[];
|
|
368
|
-
/**
|
|
369
|
-
* Per-item marks carried by a grade — for example the per-word marks of a
|
|
370
|
-
* read-aloud grade. `outcomeFromGrade` does not mirror them onto the
|
|
371
|
-
* outcome; read `outcome.grade.details`.
|
|
400
|
+
* The SDK never estimates it and never adds it up. It carries what a port
|
|
401
|
+
* sends through to `onInteraction`, beside the provenance, so the record of
|
|
402
|
+
* what a learner was shown and the record of what it cost are the same
|
|
403
|
+
* record. Numbers that are not finite and zero or more are dropped.
|
|
372
404
|
*/
|
|
373
|
-
details?: ScoringDetail[];
|
|
374
|
-
/** Corrections anchored in the learner's text. */
|
|
375
|
-
corrections?: InlineCorrection[];
|
|
376
|
-
/** Supporting observations the grader cited. */
|
|
377
|
-
evidence?: string[];
|
|
378
|
-
/** How the grader reached this verdict. */
|
|
379
|
-
rationale?: string;
|
|
380
|
-
/** The grader's own confidence — a signal for routing to human review. */
|
|
381
|
-
confidence?: 'high' | 'medium' | 'low';
|
|
382
|
-
/** The grader is unsure or the submission is atypical; route to a human. */
|
|
383
|
-
requiresHumanReview?: boolean;
|
|
384
|
-
grader?: Grader;
|
|
385
405
|
usage?: GraderUsage;
|
|
386
|
-
/** ISO 8601 timestamp of when the grade was produced. */
|
|
387
|
-
gradedAt?: string;
|
|
388
406
|
}
|
|
407
|
+
/** Why a result was not shown. */
|
|
408
|
+
type AiRefusal = 'malformed' | 'empty' | 'too-long' | 'contradicts-grade' | 'reveals-answer';
|
|
389
409
|
|
|
390
410
|
/**
|
|
391
411
|
* An xAPI Activity object — the thing a statement is about.
|
|
@@ -1388,8 +1408,13 @@ interface DeferredScoringPartial {
|
|
|
1388
1408
|
* a grade has not landed, or the attempt is still open has earned nothing
|
|
1389
1409
|
* yet, and recording it as zero is how an incomplete attempt becomes a
|
|
1390
1410
|
* plausible-looking fail.
|
|
1411
|
+
* - `grade_rejected` — a grade came back, and `outcomeFromGrade` refused it
|
|
1412
|
+
* because its numbers cannot be a grade (a `score` or `maxScore` that is not
|
|
1413
|
+
* a finite number, a `maxScore` of 0 or less, or a score outside 0 to
|
|
1414
|
+
* `maxScore`). The record is kept on `rejectedGrade`. A real grade is still
|
|
1415
|
+
* owed, so re-grade the slot: waiting will not bring one.
|
|
1391
1416
|
*/
|
|
1392
|
-
type DeferredReason = 'requires_async_grading' | 'no_response_recorded';
|
|
1417
|
+
type DeferredReason = 'requires_async_grading' | 'no_response_recorded' | 'grade_rejected';
|
|
1393
1418
|
/**
|
|
1394
1419
|
* The outcome of evaluating a learner response against an activity — the
|
|
1395
1420
|
* union `evaluate()` returns. Unlike {@link ScoringResult}, it can express
|
|
@@ -1416,6 +1441,12 @@ type ItemOutcome = {
|
|
|
1416
1441
|
maxScore: number;
|
|
1417
1442
|
/** Synchronously computable progress facts (word bounds, counts). */
|
|
1418
1443
|
partial?: DeferredScoringPartial;
|
|
1444
|
+
/**
|
|
1445
|
+
* The record `outcomeFromGrade` refused, verbatim, when `reason` is
|
|
1446
|
+
* `grade_rejected` — kept for audit and for the re-grade. Nothing on it
|
|
1447
|
+
* is mirrored onto the outcome, and nothing reads it as a grade.
|
|
1448
|
+
*/
|
|
1449
|
+
rejectedGrade?: GradeRecord;
|
|
1419
1450
|
} | {
|
|
1420
1451
|
status: 'graded';
|
|
1421
1452
|
/**
|
|
@@ -1530,8 +1561,18 @@ type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled'
|
|
|
1530
1561
|
* is never shown and never reported here.
|
|
1531
1562
|
*/
|
|
1532
1563
|
| 'ai-hint-shown'
|
|
1533
|
-
/** An AI explanation of the graded answer was shown (`provenance` when sent). */
|
|
1534
|
-
| 'ai-explanation-shown'
|
|
1564
|
+
/** An AI explanation of the graded answer was shown (`provenance`, `usage` when sent). */
|
|
1565
|
+
| 'ai-explanation-shown'
|
|
1566
|
+
/**
|
|
1567
|
+
* An AI answer was refused, and the learner was told help was not available
|
|
1568
|
+
* (`feature`, `reason` — an {@link AiRefusal} — and `hintNumber` for a hint).
|
|
1569
|
+
*
|
|
1570
|
+
* **The text itself is never carried**: a hint refused for revealing the
|
|
1571
|
+
* answer contains the answer, and this event is logged. It is how a host
|
|
1572
|
+
* watches its own model in production, where the development console warning
|
|
1573
|
+
* it accompanies is silent.
|
|
1574
|
+
*/
|
|
1575
|
+
| 'ai-help-refused' | (string & {});
|
|
1535
1576
|
/** Fired by activity components on every discrete learner interaction. */
|
|
1536
1577
|
interface InteractionEvent {
|
|
1537
1578
|
/** The kind of interaction that occurred. */
|