@intellectif/lk-core 1.4.0 → 1.6.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 (89) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +2 -0
  3. package/dist/ai-check.cjs +175 -17
  4. package/dist/ai-check.cjs.map +1 -1
  5. package/dist/ai-check.d.cts +13 -5
  6. package/dist/ai-check.d.cts.map +1 -1
  7. package/dist/ai-check.d.ts +13 -5
  8. package/dist/ai-check.d.ts.map +1 -1
  9. package/dist/ai-check.js +175 -17
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{ai-CnmLPMzd.d.ts → ai-i3jwXnpX.d.cts} +242 -7
  12. package/dist/ai-i3jwXnpX.d.cts.map +1 -0
  13. package/dist/{ai-CnmLPMzd.d.cts → ai-i3jwXnpX.d.ts} +242 -7
  14. package/dist/ai-i3jwXnpX.d.ts.map +1 -0
  15. package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-C4YRVrFi.cjs} +683 -72
  16. package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
  17. package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-OJ5m_1EB.js} +663 -88
  18. package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
  19. package/dist/{index-BqMXWuNC.d.cts → index--U7hHV0s.d.cts} +2 -2
  20. package/dist/{index-BqMXWuNC.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
  21. package/dist/{index-B54baFUA.d.ts → index-CFRgZLhH.d.cts} +45 -12
  22. package/dist/index-CFRgZLhH.d.cts.map +1 -0
  23. package/dist/{index-CVWvymVb.d.ts → index-DeD8EI1D.d.ts} +327 -3
  24. package/dist/index-DeD8EI1D.d.ts.map +1 -0
  25. package/dist/{index-CcHDTxIZ.d.ts → index-Dj92T76o.d.ts} +2 -2
  26. package/dist/{index-CcHDTxIZ.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
  27. package/dist/{index-BAfEHCnD.d.cts → index-Uh1u57yq.d.cts} +327 -3
  28. package/dist/index-Uh1u57yq.d.cts.map +1 -0
  29. package/dist/{index-2nigSf4N.d.cts → index-WBHSrIUU.d.ts} +45 -12
  30. package/dist/index-WBHSrIUU.d.ts.map +1 -0
  31. package/dist/index.cjs +88 -131
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +217 -95
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +217 -95
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +60 -118
  38. package/dist/index.js.map +1 -1
  39. package/dist/{item-group-DXiR8nOr.js → item-group-BbvvH_Ob.js} +3 -2
  40. package/dist/item-group-BbvvH_Ob.js.map +1 -0
  41. package/dist/{item-group-DroFXgLb.cjs → item-group-CQRbb8Rx.cjs} +3 -2
  42. package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
  43. package/dist/{registry-Dsx9mNsS.js → registry-B23o9UWh.js} +218 -21
  44. package/dist/registry-B23o9UWh.js.map +1 -0
  45. package/dist/{registry-DVi0LR_i.cjs → registry-Ih5n4PVZ.cjs} +265 -20
  46. package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
  47. package/dist/{schemas-DVbAOrtd.cjs → schemas-Q2dyOgc2.cjs} +77 -14
  48. package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
  49. package/dist/{schemas-Bdw0nGPo.js → schemas-yId-bJhD.js} +72 -15
  50. package/dist/schemas-yId-bJhD.js.map +1 -0
  51. package/dist/schemas.cjs +5 -2
  52. package/dist/schemas.d.cts +2 -2
  53. package/dist/schemas.d.ts +2 -2
  54. package/dist/schemas.js +4 -3
  55. package/dist/{scoring-BZqUkuDF.js → scoring-CLMdk8i9.js} +657 -3
  56. package/dist/scoring-CLMdk8i9.js.map +1 -0
  57. package/dist/{scoring-B2_HEhMT.cjs → scoring-CaPS6q8b.cjs} +722 -2
  58. package/dist/scoring-CaPS6q8b.cjs.map +1 -0
  59. package/dist/scoring.cjs +8 -2
  60. package/dist/scoring.d.cts +3 -3
  61. package/dist/scoring.d.ts +3 -3
  62. package/dist/scoring.js +3 -3
  63. package/dist/{xapi-DNzkSJ0S.cjs → xapi-BX0hvRRB.cjs} +2 -2
  64. package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
  65. package/dist/{xapi-CMTph7wG.js → xapi-Cdk9fTvV.js} +2 -2
  66. package/dist/{xapi-CMTph7wG.js.map → xapi-Cdk9fTvV.js.map} +1 -1
  67. package/dist/xapi.cjs +1 -1
  68. package/dist/xapi.d.cts +1 -1
  69. package/dist/xapi.d.ts +1 -1
  70. package/dist/xapi.js +1 -2
  71. package/package.json +2 -2
  72. package/vectors/README.md +24 -1
  73. package/vectors/scoring.json +18182 -1
  74. package/dist/ai-CnmLPMzd.d.cts.map +0 -1
  75. package/dist/ai-CnmLPMzd.d.ts.map +0 -1
  76. package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
  77. package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
  78. package/dist/index-2nigSf4N.d.cts.map +0 -1
  79. package/dist/index-B54baFUA.d.ts.map +0 -1
  80. package/dist/index-BAfEHCnD.d.cts.map +0 -1
  81. package/dist/index-CVWvymVb.d.ts.map +0 -1
  82. package/dist/item-group-DXiR8nOr.js.map +0 -1
  83. package/dist/item-group-DroFXgLb.cjs.map +0 -1
  84. package/dist/registry-DVi0LR_i.cjs.map +0 -1
  85. package/dist/registry-Dsx9mNsS.js.map +0 -1
  86. package/dist/schemas-Bdw0nGPo.js.map +0 -1
  87. package/dist/schemas-DVbAOrtd.cjs.map +0 -1
  88. package/dist/scoring-B2_HEhMT.cjs.map +0 -1
  89. package/dist/scoring-BZqUkuDF.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # @intellectif/lk-core
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#120](https://github.com/intellectif/learning-kit/pull/120) [`d5ee618`](https://github.com/intellectif/learning-kit/commit/d5ee6185d2a167794352b218486881c405b13b52) Thanks [@diego28e](https://github.com/diego28e)! - **Exporting and importing content**: list and re-point every file an item or a group uses, keep what each recording says for the author's side only, and describe content in one JSON Schema.
8
+
9
+ **Action required:** none. Every field is optional and every function new. Two cases to check:
10
+
11
+ - **A field you already store under these names.** If your content keeps a media `transcript` that is not a string, or a `script` that is not a list of `{ speaker, text }` turns, it now fails validation.
12
+ - **A `switch` over the critic's codes.** The critic has three new codes, so a `switch` over them without a `default` branch needs them.
13
+
14
+ - **Files:** `listMediaRefs(entry)` lists every media address in an activity or an item group: each one's path, kind, owner and language. It covers an item's `media`, a multiple-choice option's `media`, dictation's and read-aloud's `slowMedia`, and a stimulus's `media`, with their captions, tracks and posters. A recording with no file yet is listed with `url: null`.
15
+
16
+ ```ts
17
+ const files = listMediaRefs(entry).filter((ref) => ref.url !== null);
18
+ ```
19
+
20
+ `mapMediaRefs(entry, fn)` returns a copy with each address replaced and nothing else changed. With `(ref) => ref.url` the copy equals the input. A type you register says where it keeps files with `mediaSlots` on its descriptor.
21
+
22
+ ```ts
23
+ const imported = mapMediaRefs(entry, (ref) =>
24
+ ref.url === null ? null : storedUrlFor(ref.url)
25
+ );
26
+ ```
27
+
28
+ - **What a recording says:** an item's media, a listening option's media and a stimulus can carry an author-only `transcript` (plain text) and `script` (turns, each a `speaker` and a `text`).
29
+
30
+ - Both are removed by `redact()` and `redactItemGroup()` at every reveal level, and refused by `assertRedacted()`. No request for a learner's AI help reads them.
31
+ - `transcriptOf(holder)` gives the author-side text, the script as `Speaker: text` lines when there is no transcript. Pass it to `aiDraftsRequest` as its `source`.
32
+
33
+ ```jsonc
34
+ {
35
+ "type": "audio",
36
+ "url": "https://cdn.example/station.mp3",
37
+ "transcript": "Where does the train leave from? Platform four.",
38
+ "script": [
39
+ { "speaker": "Anna", "text": "Where does the train leave from?" },
40
+ { "speaker": "Ben", "text": "Platform four." }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ - **Practice scored in the browser:** `withoutAuthorOnly(entry)` returns an item or a group without its author-only fields, and with its key and your own fields. Unlike `redact()`, whose output scorers refuse, it scores exactly as the stored item. Every vector of the grade-stability corpus shows it. Exams still go through `redact()`.
46
+
47
+ ```ts
48
+ res.json(withoutAuthorOnly(practiceItem));
49
+ ```
50
+
51
+ - **The critic's new codes:** `media_script_differs` (a transcript and a script that say different words), `media_transcript_differs` (a dictation's or a read-aloud's recording saying other words than the item), and `media_transcript_unused` (on a picture or an embed). The AI critic now reads a recording's transcript and script. It no longer reads a caption track's address or language, a poster, or a speaker's label.
52
+ - **JSON Schema:** `contentJsonSchema()` is one draft 2020-12 document whose root is a sequence entry: any registered activity type, or an item group with typed items. `jsonSchemaFor(type, { target: 'draft-2020-12' })` gives one type's schema in that draft. Draft 7 stays the default, so the existing exports do not change.
53
+
54
+ ```ts
55
+ writeFileSync("schema.json", JSON.stringify(contentJsonSchema(), null, 2));
56
+ ```
57
+
58
+ Guide: https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md#exporting-and-importing-content-16
59
+
60
+ ## 1.5.0
61
+
62
+ ### Minor Changes
63
+
64
+ - [#112](https://github.com/intellectif/learning-kit/pull/112) [`3fc1049`](https://github.com/intellectif/learning-kit/commit/3fc1049d67e14f6f4a12c235f19952a988472978) Thanks [@diego28e](https://github.com/diego28e)! - **Assisted grading**: a model grades a written response for the record, and in an exam no model's grade counts until it has been measured against your teachers.
65
+
66
+ **Action required:** none. Nothing grades until you call it. Four union members are new: `DeferredReason` gains `awaiting_human_review`, `AiRefusal` gains `misses-criteria` and `out-of-scale`, and `AiCheckCase['feature']` gains `grading`. A `switch` over one of them without a `default` branch needs them.
67
+
68
+ - **Levels on a rubric:** a criterion can carry `levels`, lowest first, from 2 to 10, each a `label` and an optional `description`. A level scores `index / (levels − 1)`.
69
+ - `validateActivity` does not check them in 1.x, so a field of that name you already use stays valid.
70
+ - The critic warns `wr_levels_unreadable` when it cannot read them.
71
+ - `redact()` passes readable levels to the learner, `label` and `description` only.
72
+ - **The request and its check:**
73
+ - `aiGradingRequest` builds what a model is told: the response verbatim, the rubric with its levels, and a group's material.
74
+ - `checkAiGrading` accepts a grade whole or not at all. It needs every criterion once (`misses-criteria`), each on its own scale (`out-of-scale`), with corrections that quote the response (`misquotes-answer`).
75
+ - **The grade:** `gradeFromJudgement` computes the total as `gradeFromRubric` does, with the item's weights and pass line. It records the model and the prompt revision on `grader`.
76
+ - **Calibration:** `calibrationReport` measures one grader configuration (a model, a prompt revision, a rubric and a target) against people's grades of the same responses. It reports:
77
+
78
+ - exact and adjacent agreement, quadratic kappa and a confusion matrix, on levels;
79
+ - Pearson's r, mean absolute error and the standardised mean difference, everywhere;
80
+ - calibration error and the Brier score, where grades carry level probabilities;
81
+ - the degradation from two people's agreement.
82
+
83
+ It ships no thresholds.
84
+
85
+ - **Routing:** `routeGrade` decides whether a grade counts or goes to a person, and gives every reason, in a fixed order:
86
+
87
+ - no matching calibration, in an exam;
88
+ - a report below your thresholds;
89
+ - a model unsure of a level, or one that asked for review;
90
+ - low confidence;
91
+ - the audit sample, chosen by `inAuditSample`.
92
+
93
+ `outcomeFromRoutedGrade` holds a grade for a person as `deferred` / `awaiting_human_review`, with the grade kept on `pendingGrade`, so `composeAssessmentScore` keeps the attempt provisional.
94
+
95
+ - **New optional fields:**
96
+ - `levels` on a rubric criterion, its redacted form included;
97
+ - `levelProbabilities` on `CriterionScore`;
98
+ - `routing` on `GradeRecord`;
99
+ - `pendingGrade` on a deferred `ItemOutcome`.
100
+ - **`@intellectif/lk-core/ai-check`** has five grading cases, for a `grading` port.
101
+ - **The grade-stability corpus** gains 123 vectors for the new functions. The 635 before them are unchanged.
102
+
103
+ Guide: https://github.com/intellectif/learning-kit/blob/main/docs/grading.md
104
+
3
105
  ## 1.4.0
4
106
 
5
107
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,7 @@ npm install @intellectif/lk-core
25
25
  - **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.
26
26
  - **`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.
27
27
  - **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()`.
28
+ - **Assisted grading, behind a calibration gate** — `aiGradingRequest` / `checkAiGrading` hold a model's grade of a written response to every criterion of the rubric, each on its own scale (levels the author writes, or a score); `gradeFromJudgement` computes the total. `calibrationReport` measures a model, prompt, rubric and target against your teachers' grades, and `routeGrade` sends a grade to a person, for reasons it names: in an exam, until a matching calibration is supplied. No thresholds ship: they are yours.
28
29
  - **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.
29
30
  - **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.
30
31
  - **Delivery policies** — `DeliveryPolicy` says what a school lets a learner see around a question: feedback, solutions, hints and AI help. `resolveDeliveryPolicy` spells one out (an unreadable setting restricts, never allows), `validateDeliveryPolicy` checks one before it is stored, and `combineDeliveryPolicies` keeps a setting on only where every policy leaves it on.
@@ -161,6 +162,7 @@ Zod is the single runtime dependency, pinned to one exact version and private: n
161
162
  - [Delivery policies](https://github.com/intellectif/learning-kit/blob/main/docs/delivery.md) — `DeliveryPolicy`, and recording it with an attempt.
162
163
  - [Scoring policies](https://github.com/intellectif/learning-kit/blob/main/docs/scoring.md) — `ItemScoringPolicy`: tries, which counts, what hints cost, and scoring them on a server.
163
164
  - [AI help for learners](https://github.com/intellectif/learning-kit/blob/main/docs/ai.md) — the AI contract, the checks, and the `ai-check` kit for your prompts.
165
+ - [Grading with a model](https://github.com/intellectif/learning-kit/blob/main/docs/grading.md) — a model's grade for the record: levels, calibration against your teachers, and where a grade goes.
164
166
  - [Project README](https://github.com/intellectif/learning-kit#readme) — the full picture, including the React renderers.
165
167
  - [Contributing](https://github.com/intellectif/learning-kit/blob/main/CONTRIBUTING.md) — adding a new activity type.
166
168
 
package/dist/ai-check.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ai_writing = require("./ai-writing-G6tnCGzw.cjs");
3
- const require_scoring = require("./scoring-B2_HEhMT.cjs");
2
+ const require_ai_writing = require("./ai-writing-C4YRVrFi.cjs");
3
+ const require_scoring = require("./scoring-CaPS6q8b.cjs");
4
4
  //#region src/ai-check/cases.ts
5
5
  /**
6
6
  * The items the cases are built from. Small, ordinary language-course content
@@ -17,7 +17,10 @@ const require_scoring = require("./scoring-B2_HEhMT.cjs");
17
17
  * twice — so a correction of words nobody wrote is caught;
18
18
  * - questions in item groups: a reading passage that prints a question's
19
19
  * answer, which a hint must point to without quoting, and a run of blanks
20
- * whose instructions only the group states.
20
+ * whose instructions only the group states;
21
+ * - essays to grade on a rubric of three criteria, two judged on levels and
22
+ * one on a score, so a grade that drops a criterion or leaves a scale is
23
+ * caught.
21
24
  */
22
25
  const capital = {
23
26
  schemaVersion: "1.0",
@@ -205,6 +208,117 @@ const weekend = {
205
208
  }
206
209
  ] }
207
210
  };
211
+ /**
212
+ * The weekend task set as an exam question: graded on three criteria, two on
213
+ * levels and one on a score. Three, so a prompt that names only the ones it
214
+ * expects drops one; two scales, so a grade must keep to each.
215
+ */
216
+ const weekendExam = {
217
+ schemaVersion: "1.0",
218
+ type: "written-response",
219
+ id: "ai-check-writing-exam",
220
+ title: "Your weekend",
221
+ prompt: "Write a few sentences about what you did last weekend.",
222
+ minWords: 15,
223
+ maxWords: 120,
224
+ languageTarget: "en-A2",
225
+ rubric: { criteria: [
226
+ {
227
+ name: "Task",
228
+ description: "Says what the learner did last weekend, in order",
229
+ weight: 1,
230
+ levels: [
231
+ {
232
+ label: "Not met",
233
+ description: "Not about last weekend"
234
+ },
235
+ {
236
+ label: "Partly met",
237
+ description: "About the weekend, but thin or out of order"
238
+ },
239
+ {
240
+ label: "Met",
241
+ description: "Says what they did, in order"
242
+ }
243
+ ]
244
+ },
245
+ {
246
+ name: "Grammar",
247
+ description: "Past tenses, articles, agreement",
248
+ weight: 2,
249
+ levels: [
250
+ {
251
+ label: "Limited",
252
+ description: "Errors in most sentences, some blocking meaning"
253
+ },
254
+ {
255
+ label: "Basic",
256
+ description: "Frequent errors in past tenses and articles"
257
+ },
258
+ {
259
+ label: "Good",
260
+ description: "Some errors, none that blocks meaning"
261
+ },
262
+ {
263
+ label: "Very good",
264
+ description: "Past tenses, articles and agreement almost always right"
265
+ }
266
+ ]
267
+ },
268
+ {
269
+ name: "Vocabulary",
270
+ description: "Everyday words, used correctly",
271
+ weight: 1
272
+ }
273
+ ] }
274
+ };
275
+ /** The tides summary set as an exam question, graded against its group's passage. */
276
+ const tidesSummaryExam = {
277
+ schemaVersion: "1.0",
278
+ type: "written-response",
279
+ id: "ai-check-tides-summary-exam",
280
+ title: "Summary",
281
+ prompt: "Summarise the text in one or two sentences.",
282
+ minWords: 10,
283
+ maxWords: 60,
284
+ languageTarget: "en-B1",
285
+ rubric: { criteria: [{
286
+ name: "Content",
287
+ description: "What the text says, and nothing it does not",
288
+ weight: 2,
289
+ levels: [
290
+ {
291
+ label: "Wrong",
292
+ description: "Misstates what the text says"
293
+ },
294
+ {
295
+ label: "Partly right",
296
+ description: "Gets one of its points wrong, or leaves it out"
297
+ },
298
+ {
299
+ label: "Right",
300
+ description: "States its points as the text does"
301
+ }
302
+ ]
303
+ }, {
304
+ name: "Language",
305
+ description: "Clear sentences, in the learner's own words",
306
+ weight: 1
307
+ }] }
308
+ };
309
+ const tidesExam = {
310
+ schemaVersion: "1.0",
311
+ type: "item-group",
312
+ id: "ai-check-tides-exam",
313
+ title: "Part 2 — Reading",
314
+ stimulus: {
315
+ id: "ai-check-tides-exam-text",
316
+ kind: "text",
317
+ title: "Tides",
318
+ body: TIDES
319
+ },
320
+ items: [springTides, tidesSummaryExam]
321
+ };
208
322
  const weather = {
209
323
  schemaVersion: "1.0",
210
324
  type: "read-aloud",
@@ -492,6 +606,21 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale, task
492
606
  request
493
607
  };
494
608
  }
609
+ function gradingCase(id, about, text, extra = {}) {
610
+ const built = require_ai_writing.aiGradingRequest({
611
+ data: extra.data ?? weekendExam,
612
+ response: drafted(text),
613
+ ...extra.group ? { group: extra.group } : {},
614
+ ...extra.learnerLocale !== void 0 ? { learnerLocale: extra.learnerLocale } : {}
615
+ });
616
+ if (!built.ok) throw new Error(`ai-check: no grading request for "${id}": ${built.problem}`);
617
+ return {
618
+ id,
619
+ feature: "grading",
620
+ about,
621
+ request: built.request
622
+ };
623
+ }
495
624
  function coachingCase(id, about, marks, learnerLocale) {
496
625
  const assessment = marks.assessment ?? forecast(true);
497
626
  const grade = require_scoring.gradeReadAloud(weather, {
@@ -528,8 +657,9 @@ function coachingCase(id, about, marks, learnerLocale) {
528
657
  /**
529
658
  * The cases {@link runAiCheck} runs: every type the SDK explains, answered
530
659
  * right, wrong and — where a type can be — partly right; every type it hints
531
- * for, before an answer and after a wrong one; and drafts of an essay, with
532
- * mistakes, without, and revised.
660
+ * for, before an answer and after a wrong one; drafts of an essay, with
661
+ * mistakes, without, and revised; and essays to grade, on the task, with
662
+ * mistakes to quote, off the task, and against a passage.
533
663
  *
534
664
  * They are ordinary calls. What makes them a test is that the SDK knows the
535
665
  * answer to each, so it can check what your model writes about it.
@@ -652,7 +782,15 @@ function aiCheckCases() {
652
782
  locale: "en",
653
783
  instructions: "Focus on what Maria and her grandmother buy.",
654
784
  settings: { "read-aloud": READ_ALOUD_SETTINGS }
655
- })
785
+ }),
786
+ gradingCase("grading-on-task", "An essay that does the task, with one slip in its tenses", "Last weekend I went to the market with my sister and we bought some apples. In the afternoon we watched a film at home, and on Sunday I visit my grandmother."),
787
+ gradingCase("grading-mistakes", "An essay with tense, article and agreement mistakes, each to quote as written", "Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema."),
788
+ gradingCase("grading-off-task", "An essay about something other than last weekend", "My favourite food is pizza. I like it with cheese and tomato, and I eat it every Friday with my family at a small restaurant near our house."),
789
+ gradingCase("grading-summary", "A summary graded against its group's passage, getting the spring tides wrong", "The tide comes in twice a day because the moon pulls the ocean. The highest tides come only at full moon.", {
790
+ data: tidesSummaryExam,
791
+ group: tidesExam
792
+ }),
793
+ gradingCase("grading-in-spanish", "The essay with mistakes, with comments asked for in Spanish", "Last weekend I go to the market and buyed a apple. My friends was there too, so we don't go home early. Then I go to the cinema.", { learnerLocale: "es" })
656
794
  ];
657
795
  }
658
796
  //#endregion
@@ -681,6 +819,7 @@ function aiCheckCases() {
681
819
  * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),
682
820
  * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),
683
821
  * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),
822
+ * grading: (request) => callMyModel(GRADING_PROMPT, request),
684
823
  * });
685
824
  * console.log(formatAiCheckReport(report));
686
825
  * if (report.refused > 0 || report.errors > 0) {
@@ -693,18 +832,27 @@ function aiCheckCases() {
693
832
  * to take most seriously — that prompt gives answers away. Next come
694
833
  * `misquotes-answer` on a writing case — that prompt corrects words the learner
695
834
  * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches
696
- * words the engine heard as right, or sounds it never reported.
835
+ * words the engine heard as right, or sounds it never reported. On a grading
836
+ * case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one
837
+ * that leaves a criterion out grades a different task, and one off a
838
+ * criterion's scale grades on a scale the author did not write.
839
+ *
840
+ * A grading case the SDK accepts is a reply of the right shape, not a right
841
+ * grade: whether a model grades as your teachers do is what a calibration
842
+ * measures (`calibrationReport`), on your own responses.
697
843
  */
698
- const REFUSALS = [
699
- "malformed",
700
- "empty",
701
- "too-long",
702
- "contradicts-grade",
703
- "reveals-answer",
704
- "misquotes-answer",
705
- "contradicts-marks",
706
- "contradicts-item"
707
- ];
844
+ const REFUSALS = Object.keys({
845
+ malformed: true,
846
+ empty: true,
847
+ "too-long": true,
848
+ "contradicts-grade": true,
849
+ "reveals-answer": true,
850
+ "misquotes-answer": true,
851
+ "contradicts-marks": true,
852
+ "contradicts-item": true,
853
+ "misses-criteria": true,
854
+ "out-of-scale": true
855
+ });
708
856
  /** The port a case is for. */
709
857
  function portFor(ports, one) {
710
858
  switch (one.request.feature) {
@@ -713,6 +861,7 @@ function portFor(ports, one) {
713
861
  case "pronunciation-coaching": return ports.pronunciationCoaching;
714
862
  case "item-critique": return ports.critique;
715
863
  case "draft-generation": return ports.drafts;
864
+ case "grading": return ports.grading;
716
865
  default: return ports.writingFeedback;
717
866
  }
718
867
  }
@@ -735,6 +884,15 @@ async function runCase(ports, one) {
735
884
  };
736
885
  }
737
886
  const ms = now() - started;
887
+ if (one.request.feature === "grading") {
888
+ const checked = require_ai_writing.checkAiGrading(raw, one.request);
889
+ return checked.ok ? {
890
+ case: one,
891
+ ok: true,
892
+ grading: checked.judgement,
893
+ ms
894
+ } : refused(one, raw, checked.refusal, ms);
895
+ }
738
896
  if (one.request.feature === "draft-generation") {
739
897
  let next = 0;
740
898
  const checked = require_ai_writing.checkAiDrafts(raw, one.request, { newId: () => {