@intellectif/lk-core 1.3.0 → 1.5.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 (85) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +2 -0
  3. package/dist/{ai-B222dtHH.d.ts → ai-BYryFkF9.d.cts} +462 -5
  4. package/dist/ai-BYryFkF9.d.cts.map +1 -0
  5. package/dist/{ai-B222dtHH.d.cts → ai-BYryFkF9.d.ts} +462 -5
  6. package/dist/ai-BYryFkF9.d.ts.map +1 -0
  7. package/dist/ai-check.cjs +274 -23
  8. package/dist/ai-check.cjs.map +1 -1
  9. package/dist/ai-check.d.cts +13 -5
  10. package/dist/ai-check.d.cts.map +1 -1
  11. package/dist/ai-check.d.ts +13 -5
  12. package/dist/ai-check.d.ts.map +1 -1
  13. package/dist/ai-check.js +274 -23
  14. package/dist/ai-check.js.map +1 -1
  15. package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-Cen7-3qs.cjs} +520 -58
  16. package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
  17. package/dist/{ai-writing-CCBjTOM3.js → ai-writing-D8PdhJJz.js} +513 -75
  18. package/dist/ai-writing-D8PdhJJz.js.map +1 -0
  19. package/dist/{index-Dyu66R4-.d.cts → index-Bg0aeWtM.d.cts} +327 -3
  20. package/dist/index-Bg0aeWtM.d.cts.map +1 -0
  21. package/dist/{index-BWPRPIXL.d.ts → index-CxpiWedW.d.ts} +7 -217
  22. package/dist/index-CxpiWedW.d.ts.map +1 -0
  23. package/dist/{index-CXyOV8sm.d.cts → index-D7R3Ll8R.d.cts} +2 -2
  24. package/dist/{index-CXyOV8sm.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
  25. package/dist/{index-UGxRSopE.d.ts → index-DUaW1Tj5.d.ts} +327 -3
  26. package/dist/index-DUaW1Tj5.d.ts.map +1 -0
  27. package/dist/{index-O4_ExlAD.d.cts → index-qMbxed1B.d.cts} +7 -217
  28. package/dist/index-qMbxed1B.d.cts.map +1 -0
  29. package/dist/{index-CegM8o15.d.ts → index-wMc1E_aM.d.ts} +2 -2
  30. package/dist/{index-CegM8o15.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
  31. package/dist/index.cjs +32 -130
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +115 -97
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +115 -97
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +9 -117
  38. package/dist/index.js.map +1 -1
  39. package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
  40. package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
  41. package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
  42. package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
  43. package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
  44. package/dist/registry-Ce00A_OW.cjs.map +1 -0
  45. package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
  46. package/dist/registry-DaZRpqjW.js.map +1 -0
  47. package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
  48. package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
  49. package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
  50. package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
  51. package/dist/schemas.cjs +2 -2
  52. package/dist/schemas.d.cts +1 -1
  53. package/dist/schemas.d.ts +1 -1
  54. package/dist/schemas.js +2 -2
  55. package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
  56. package/dist/scoring-BKNNTz-s.js.map +1 -0
  57. package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
  58. package/dist/scoring-lKQVYsxy.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-NYrTlpID.cjs} +2 -2
  64. package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
  65. package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
  66. package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.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-B222dtHH.d.cts.map +0 -1
  75. package/dist/ai-B222dtHH.d.ts.map +0 -1
  76. package/dist/ai-writing-CCBjTOM3.js.map +0 -1
  77. package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
  78. package/dist/index-BWPRPIXL.d.ts.map +0 -1
  79. package/dist/index-Dyu66R4-.d.cts.map +0 -1
  80. package/dist/index-O4_ExlAD.d.cts.map +0 -1
  81. package/dist/index-UGxRSopE.d.ts.map +0 -1
  82. package/dist/registry-DVi0LR_i.cjs.map +0 -1
  83. package/dist/registry-Dsx9mNsS.js.map +0 -1
  84. package/dist/scoring-B2_HEhMT.cjs.map +0 -1
  85. package/dist/scoring-BZqUkuDF.js.map +0 -1
package/dist/ai-check.js CHANGED
@@ -1,5 +1,5 @@
1
- import { D as checkAiCoaching, E as aiCoachingRequest, L as checkAiExplanation, M as aiExplanationRequest, P as aiHintRequest, R as checkAiHint, c as aiDraftsRequest, h as checkAiCritique, i as checkAiWritingFeedback, l as checkAiDrafts, m as aiCritiqueRequest, r as aiWritingFeedbackRequest } from "./ai-writing-CCBjTOM3.js";
2
- import { o as gradeReadAloud } from "./scoring-BZqUkuDF.js";
1
+ import { A as checkAiCoaching, B as checkAiExplanation, F as aiExplanationRequest, L as aiHintRequest, V as checkAiHint, _ as aiCritiqueRequest, a as checkAiGrading, d as aiDraftsRequest, f as checkAiDrafts, i as aiGradingRequest, k as aiCoachingRequest, n as checkAiWritingFeedback, t as aiWritingFeedbackRequest, v as checkAiCritique } from "./ai-writing-D8PdhJJz.js";
2
+ import { o as gradeReadAloud } from "./scoring-BKNNTz-s.js";
3
3
  //#region src/ai-check/cases.ts
4
4
  /**
5
5
  * The items the cases are built from. Small, ordinary language-course content
@@ -13,7 +13,13 @@ import { o as gradeReadAloud } from "./scoring-BZqUkuDF.js";
13
13
  * - a passage with more than one blank, so a hint can be about the wrong one;
14
14
  * - a short essay whose mistakes a model must quote exactly as the learner
15
15
  * wrote them — straight apostrophes a model likes to curl, a mistake made
16
- * twice — so a correction of words nobody wrote is caught.
16
+ * twice — so a correction of words nobody wrote is caught;
17
+ * - questions in item groups: a reading passage that prints a question's
18
+ * answer, which a hint must point to without quoting, and a run of blanks
19
+ * whose instructions only the group states;
20
+ * - essays to grade on a rubric of three criteria, two judged on levels and
21
+ * one on a score, so a grade that drops a criterion or leaves a scale is
22
+ * caught.
17
23
  */
18
24
  const capital = {
19
25
  schemaVersion: "1.0",
@@ -94,6 +100,86 @@ const listening = {
94
100
  title: "Listen and type",
95
101
  transcript: "The train leaves at half past nine."
96
102
  };
103
+ /**
104
+ * A reading group: a passage, a question it answers word for word, and a
105
+ * summary of it. The question's answer is printed in the passage, so a hint
106
+ * that quotes the passage where it answers is refused.
107
+ */
108
+ const TIDES = "Along most coasts the tide comes in twice a day. It is pulled by the moon: as the Earth turns, the ocean nearest the moon bulges towards it, and so does the ocean on the far side. The highest tides, called spring tides, come when the sun and the moon line up.";
109
+ const springTides = {
110
+ schemaVersion: "1.0",
111
+ type: "multiple-choice",
112
+ id: "ai-check-tides",
113
+ title: "Spring tides",
114
+ question: "When do the highest tides come?",
115
+ mode: "single",
116
+ scoringStrategy: "all-or-nothing",
117
+ options: [
118
+ {
119
+ id: "line-up",
120
+ text: "When the sun and the moon line up",
121
+ isCorrect: true
122
+ },
123
+ {
124
+ id: "full-moon",
125
+ text: "Only at full moon",
126
+ isCorrect: false
127
+ },
128
+ {
129
+ id: "winter",
130
+ text: "In winter",
131
+ isCorrect: false
132
+ }
133
+ ]
134
+ };
135
+ const tidesSummary = {
136
+ schemaVersion: "1.0",
137
+ type: "written-response",
138
+ id: "ai-check-tides-summary",
139
+ title: "Summary",
140
+ prompt: "Summarise the text in one or two sentences.",
141
+ minWords: 10,
142
+ maxWords: 60,
143
+ languageTarget: "en-B1"
144
+ };
145
+ const tides = {
146
+ schemaVersion: "1.0",
147
+ type: "item-group",
148
+ id: "ai-check-tides-group",
149
+ title: "Part 2 — Reading",
150
+ stimulus: {
151
+ id: "ai-check-tides-text",
152
+ kind: "text",
153
+ title: "Tides",
154
+ body: TIDES
155
+ },
156
+ items: [springTides, tidesSummary]
157
+ };
158
+ /** A part whose questions share instructions the questions themselves never state. */
159
+ const formation = {
160
+ schemaVersion: "1.0",
161
+ type: "fill-in-the-blanks",
162
+ id: "ai-check-formation",
163
+ title: "Word formation",
164
+ passage: "She smiled {{a}} when she heard the news. (HAPPY)",
165
+ blanks: [{
166
+ id: "a",
167
+ acceptedAnswers: ["happily"]
168
+ }],
169
+ scoringStrategy: "all-or-nothing"
170
+ };
171
+ const formationPart = {
172
+ schemaVersion: "1.0",
173
+ type: "item-group",
174
+ id: "ai-check-formation-part",
175
+ title: "Part 1 — Word formation",
176
+ stimulus: {
177
+ id: "ai-check-formation-instructions",
178
+ kind: "text",
179
+ body: "Use the word in capitals at the end of each sentence to form a word that fits the gap."
180
+ },
181
+ items: [formation]
182
+ };
97
183
  const weekend = {
98
184
  schemaVersion: "1.0",
99
185
  type: "written-response",
@@ -121,6 +207,117 @@ const weekend = {
121
207
  }
122
208
  ] }
123
209
  };
210
+ /**
211
+ * The weekend task set as an exam question: graded on three criteria, two on
212
+ * levels and one on a score. Three, so a prompt that names only the ones it
213
+ * expects drops one; two scales, so a grade must keep to each.
214
+ */
215
+ const weekendExam = {
216
+ schemaVersion: "1.0",
217
+ type: "written-response",
218
+ id: "ai-check-writing-exam",
219
+ title: "Your weekend",
220
+ prompt: "Write a few sentences about what you did last weekend.",
221
+ minWords: 15,
222
+ maxWords: 120,
223
+ languageTarget: "en-A2",
224
+ rubric: { criteria: [
225
+ {
226
+ name: "Task",
227
+ description: "Says what the learner did last weekend, in order",
228
+ weight: 1,
229
+ levels: [
230
+ {
231
+ label: "Not met",
232
+ description: "Not about last weekend"
233
+ },
234
+ {
235
+ label: "Partly met",
236
+ description: "About the weekend, but thin or out of order"
237
+ },
238
+ {
239
+ label: "Met",
240
+ description: "Says what they did, in order"
241
+ }
242
+ ]
243
+ },
244
+ {
245
+ name: "Grammar",
246
+ description: "Past tenses, articles, agreement",
247
+ weight: 2,
248
+ levels: [
249
+ {
250
+ label: "Limited",
251
+ description: "Errors in most sentences, some blocking meaning"
252
+ },
253
+ {
254
+ label: "Basic",
255
+ description: "Frequent errors in past tenses and articles"
256
+ },
257
+ {
258
+ label: "Good",
259
+ description: "Some errors, none that blocks meaning"
260
+ },
261
+ {
262
+ label: "Very good",
263
+ description: "Past tenses, articles and agreement almost always right"
264
+ }
265
+ ]
266
+ },
267
+ {
268
+ name: "Vocabulary",
269
+ description: "Everyday words, used correctly",
270
+ weight: 1
271
+ }
272
+ ] }
273
+ };
274
+ /** The tides summary set as an exam question, graded against its group's passage. */
275
+ const tidesSummaryExam = {
276
+ schemaVersion: "1.0",
277
+ type: "written-response",
278
+ id: "ai-check-tides-summary-exam",
279
+ title: "Summary",
280
+ prompt: "Summarise the text in one or two sentences.",
281
+ minWords: 10,
282
+ maxWords: 60,
283
+ languageTarget: "en-B1",
284
+ rubric: { criteria: [{
285
+ name: "Content",
286
+ description: "What the text says, and nothing it does not",
287
+ weight: 2,
288
+ levels: [
289
+ {
290
+ label: "Wrong",
291
+ description: "Misstates what the text says"
292
+ },
293
+ {
294
+ label: "Partly right",
295
+ description: "Gets one of its points wrong, or leaves it out"
296
+ },
297
+ {
298
+ label: "Right",
299
+ description: "States its points as the text does"
300
+ }
301
+ ]
302
+ }, {
303
+ name: "Language",
304
+ description: "Clear sentences, in the learner's own words",
305
+ weight: 1
306
+ }] }
307
+ };
308
+ const tidesExam = {
309
+ schemaVersion: "1.0",
310
+ type: "item-group",
311
+ id: "ai-check-tides-exam",
312
+ title: "Part 2 — Reading",
313
+ stimulus: {
314
+ id: "ai-check-tides-exam-text",
315
+ kind: "text",
316
+ title: "Tides",
317
+ body: TIDES
318
+ },
319
+ items: [springTides, tidesSummaryExam]
320
+ };
124
321
  const weather = {
125
322
  schemaVersion: "1.0",
126
323
  type: "read-aloud",
@@ -363,10 +560,11 @@ const READ_ALOUD_SETTINGS = {
363
560
  }] }
364
561
  };
365
562
  /** Builds a case, failing loudly rather than shipping a case with no request. */
366
- function explanationCase(id, about, data, response) {
563
+ function explanationCase(id, about, data, response, group) {
367
564
  const request = aiExplanationRequest({
368
565
  data,
369
- response
566
+ response,
567
+ ...group ? { group } : {}
370
568
  });
371
569
  if (request === null) throw new Error(`ai-check: no explanation request for "${id}"`);
372
570
  return {
@@ -376,11 +574,12 @@ function explanationCase(id, about, data, response) {
376
574
  request
377
575
  };
378
576
  }
379
- function hintCase(id, about, data, response, previousHints = []) {
577
+ function hintCase(id, about, data, response, previousHints = [], group) {
380
578
  const request = aiHintRequest({
381
579
  data,
382
580
  response,
383
- previousHints
581
+ previousHints,
582
+ ...group ? { group } : {}
384
583
  });
385
584
  if (request === null) throw new Error(`ai-check: no hint request for "${id}"`);
386
585
  return {
@@ -390,11 +589,12 @@ function hintCase(id, about, data, response, previousHints = []) {
390
589
  request
391
590
  };
392
591
  }
393
- function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
592
+ function writingCase(id, about, text, previousFeedback = [], learnerLocale, task = { data: weekend }) {
394
593
  const request = aiWritingFeedbackRequest({
395
- data: weekend,
594
+ data: task.data,
396
595
  response: drafted(text),
397
596
  previousFeedback,
597
+ ...task.group ? { group: task.group } : {},
398
598
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
399
599
  });
400
600
  if (request === null) throw new Error(`ai-check: no writing feedback request for "${id}"`);
@@ -405,6 +605,21 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
405
605
  request
406
606
  };
407
607
  }
608
+ function gradingCase(id, about, text, extra = {}) {
609
+ const built = aiGradingRequest({
610
+ data: extra.data ?? weekendExam,
611
+ response: drafted(text),
612
+ ...extra.group ? { group: extra.group } : {},
613
+ ...extra.learnerLocale !== void 0 ? { learnerLocale: extra.learnerLocale } : {}
614
+ });
615
+ if (!built.ok) throw new Error(`ai-check: no grading request for "${id}": ${built.problem}`);
616
+ return {
617
+ id,
618
+ feature: "grading",
619
+ about,
620
+ request: built.request
621
+ };
622
+ }
408
623
  function coachingCase(id, about, marks, learnerLocale) {
409
624
  const assessment = marks.assessment ?? forecast(true);
410
625
  const grade = gradeReadAloud(weather, {
@@ -441,8 +656,9 @@ function coachingCase(id, about, marks, learnerLocale) {
441
656
  /**
442
657
  * The cases {@link runAiCheck} runs: every type the SDK explains, answered
443
658
  * right, wrong and — where a type can be — partly right; every type it hints
444
- * for, before an answer and after a wrong one; and drafts of an essay, with
445
- * mistakes, without, and revised.
659
+ * for, before an answer and after a wrong one; drafts of an essay, with
660
+ * mistakes, without, and revised; and essays to grade, on the task, with
661
+ * mistakes to quote, off the task, and against a passage.
446
662
  *
447
663
  * They are ordinary calls. What makes them a test is that the SDK knows the
448
664
  * answer to each, so it can check what your model writes about it.
@@ -477,6 +693,7 @@ function aiCheckCases() {
477
693
  })),
478
694
  explanationCase("dictation-near", "Typed the sentence with two words wrong", listening, wrote("The train leaves at half past five")),
479
695
  explanationCase("dictation-correct", "Typed the sentence correctly", listening, wrote("The train leaves at half past nine.")),
696
+ explanationCase("mc-reading-wrong", "Chose 'Only at full moon', on a question about its group's passage", springTides, chose(["full-moon"]), tides),
480
697
  hintCase("mc-hint-1", "Has not answered yet", capital, chose([])),
481
698
  hintCase("mc-hint-2", "Asked again after choosing Seville", capital, chose(["seville"]), ["Think about where the government sits."]),
482
699
  hintCase("fib-hint-1", "Both blanks empty", introduction, typed({})),
@@ -486,10 +703,16 @@ function aiCheckCases() {
486
703
  }), ["Look again at the first blank."]),
487
704
  hintCase("gap-hint-1", "Neither gap chosen, in Spanish", landscape, picked({})),
488
705
  hintCase("gap-hint-2", "Asked again with the first gap wrong", landscape, picked({ g1: "campo" }), ["Piensa en la forma del terreno."]),
706
+ hintCase("mc-reading-hint", "Has not answered a question whose answer its group's passage prints", springTides, chose([]), [], tides),
707
+ hintCase("fib-formation-hint", "A word-formation blank, under the instructions its group gives", formation, typed({}), [], formationPart),
489
708
  writingCase("writing-mistakes", "A first draft with tense, article and agreement mistakes", "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."),
490
709
  writingCase("writing-clean", "A first draft with no mistakes to correct", "Last weekend I went to the market and bought an apple. My friends were there too, so we stayed until the evening. Then we went to the cinema."),
491
710
  writingCase("writing-revised", "A second draft that fixed some mistakes", "Last weekend I went to the market and bought a apple. My friends was there too, so we didn't go home early.", ["Check your past tenses: \"go\" and \"buyed\" should be past forms."]),
492
711
  writingCase("writing-feedback-in-spanish", "A first draft, with feedback asked for in Spanish", "Last weekend I visit my grandmother. She cook a big lunch and we eat together in the garden.", [], "es"),
712
+ writingCase("writing-summary", "A summary of its group's passage that gets 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.", [], void 0, {
713
+ data: tidesSummary,
714
+ group: tides
715
+ }),
493
716
  coachingCase("coaching-slips", "A reading with a mispronounced \"th\" and a word left out", { assessment: forecast(true) }),
494
717
  coachingCase("coaching-clean", "A reading the engine marked correct throughout", { assessment: forecast(false) }),
495
718
  coachingCase("coaching-from-grade", "The same slips, from a stored grade: marks without sounds", { fromGradeOnly: true }),
@@ -558,7 +781,15 @@ function aiCheckCases() {
558
781
  locale: "en",
559
782
  instructions: "Focus on what Maria and her grandmother buy.",
560
783
  settings: { "read-aloud": READ_ALOUD_SETTINGS }
561
- })
784
+ }),
785
+ 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."),
786
+ 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."),
787
+ 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."),
788
+ 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.", {
789
+ data: tidesSummaryExam,
790
+ group: tidesExam
791
+ }),
792
+ 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" })
562
793
  ];
563
794
  }
564
795
  //#endregion
@@ -587,6 +818,7 @@ function aiCheckCases() {
587
818
  * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),
588
819
  * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),
589
820
  * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),
821
+ * grading: (request) => callMyModel(GRADING_PROMPT, request),
590
822
  * });
591
823
  * console.log(formatAiCheckReport(report));
592
824
  * if (report.refused > 0 || report.errors > 0) {
@@ -599,18 +831,27 @@ function aiCheckCases() {
599
831
  * to take most seriously — that prompt gives answers away. Next come
600
832
  * `misquotes-answer` on a writing case — that prompt corrects words the learner
601
833
  * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches
602
- * words the engine heard as right, or sounds it never reported.
834
+ * words the engine heard as right, or sounds it never reported. On a grading
835
+ * case, `misses-criteria` and `out-of-scale` mean a grade nobody can use: one
836
+ * that leaves a criterion out grades a different task, and one off a
837
+ * criterion's scale grades on a scale the author did not write.
838
+ *
839
+ * A grading case the SDK accepts is a reply of the right shape, not a right
840
+ * grade: whether a model grades as your teachers do is what a calibration
841
+ * measures (`calibrationReport`), on your own responses.
603
842
  */
604
- const REFUSALS = [
605
- "malformed",
606
- "empty",
607
- "too-long",
608
- "contradicts-grade",
609
- "reveals-answer",
610
- "misquotes-answer",
611
- "contradicts-marks",
612
- "contradicts-item"
613
- ];
843
+ const REFUSALS = Object.keys({
844
+ malformed: true,
845
+ empty: true,
846
+ "too-long": true,
847
+ "contradicts-grade": true,
848
+ "reveals-answer": true,
849
+ "misquotes-answer": true,
850
+ "contradicts-marks": true,
851
+ "contradicts-item": true,
852
+ "misses-criteria": true,
853
+ "out-of-scale": true
854
+ });
614
855
  /** The port a case is for. */
615
856
  function portFor(ports, one) {
616
857
  switch (one.request.feature) {
@@ -619,6 +860,7 @@ function portFor(ports, one) {
619
860
  case "pronunciation-coaching": return ports.pronunciationCoaching;
620
861
  case "item-critique": return ports.critique;
621
862
  case "draft-generation": return ports.drafts;
863
+ case "grading": return ports.grading;
622
864
  default: return ports.writingFeedback;
623
865
  }
624
866
  }
@@ -641,6 +883,15 @@ async function runCase(ports, one) {
641
883
  };
642
884
  }
643
885
  const ms = now() - started;
886
+ if (one.request.feature === "grading") {
887
+ const checked = checkAiGrading(raw, one.request);
888
+ return checked.ok ? {
889
+ case: one,
890
+ ok: true,
891
+ grading: checked.judgement,
892
+ ms
893
+ } : refused(one, raw, checked.refusal, ms);
894
+ }
644
895
  if (one.request.feature === "draft-generation") {
645
896
  let next = 0;
646
897
  const checked = checkAiDrafts(raw, one.request, { newId: () => {