@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.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ai_writing = require("./ai-writing-DCVkv4Zj.cjs");
3
- const require_scoring = require("./scoring-B2_HEhMT.cjs");
2
+ const require_ai_writing = require("./ai-writing-Cen7-3qs.cjs");
3
+ const require_scoring = require("./scoring-lKQVYsxy.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
@@ -14,7 +14,13 @@ const require_scoring = require("./scoring-B2_HEhMT.cjs");
14
14
  * - a passage with more than one blank, so a hint can be about the wrong one;
15
15
  * - a short essay whose mistakes a model must quote exactly as the learner
16
16
  * wrote them — straight apostrophes a model likes to curl, a mistake made
17
- * twice — so a correction of words nobody wrote is caught.
17
+ * twice — so a correction of words nobody wrote is caught;
18
+ * - questions in item groups: a reading passage that prints a question's
19
+ * answer, which a hint must point to without quoting, and a run of blanks
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.
18
24
  */
19
25
  const capital = {
20
26
  schemaVersion: "1.0",
@@ -95,6 +101,86 @@ const listening = {
95
101
  title: "Listen and type",
96
102
  transcript: "The train leaves at half past nine."
97
103
  };
104
+ /**
105
+ * A reading group: a passage, a question it answers word for word, and a
106
+ * summary of it. The question's answer is printed in the passage, so a hint
107
+ * that quotes the passage where it answers is refused.
108
+ */
109
+ 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.";
110
+ const springTides = {
111
+ schemaVersion: "1.0",
112
+ type: "multiple-choice",
113
+ id: "ai-check-tides",
114
+ title: "Spring tides",
115
+ question: "When do the highest tides come?",
116
+ mode: "single",
117
+ scoringStrategy: "all-or-nothing",
118
+ options: [
119
+ {
120
+ id: "line-up",
121
+ text: "When the sun and the moon line up",
122
+ isCorrect: true
123
+ },
124
+ {
125
+ id: "full-moon",
126
+ text: "Only at full moon",
127
+ isCorrect: false
128
+ },
129
+ {
130
+ id: "winter",
131
+ text: "In winter",
132
+ isCorrect: false
133
+ }
134
+ ]
135
+ };
136
+ const tidesSummary = {
137
+ schemaVersion: "1.0",
138
+ type: "written-response",
139
+ id: "ai-check-tides-summary",
140
+ title: "Summary",
141
+ prompt: "Summarise the text in one or two sentences.",
142
+ minWords: 10,
143
+ maxWords: 60,
144
+ languageTarget: "en-B1"
145
+ };
146
+ const tides = {
147
+ schemaVersion: "1.0",
148
+ type: "item-group",
149
+ id: "ai-check-tides-group",
150
+ title: "Part 2 — Reading",
151
+ stimulus: {
152
+ id: "ai-check-tides-text",
153
+ kind: "text",
154
+ title: "Tides",
155
+ body: TIDES
156
+ },
157
+ items: [springTides, tidesSummary]
158
+ };
159
+ /** A part whose questions share instructions the questions themselves never state. */
160
+ const formation = {
161
+ schemaVersion: "1.0",
162
+ type: "fill-in-the-blanks",
163
+ id: "ai-check-formation",
164
+ title: "Word formation",
165
+ passage: "She smiled {{a}} when she heard the news. (HAPPY)",
166
+ blanks: [{
167
+ id: "a",
168
+ acceptedAnswers: ["happily"]
169
+ }],
170
+ scoringStrategy: "all-or-nothing"
171
+ };
172
+ const formationPart = {
173
+ schemaVersion: "1.0",
174
+ type: "item-group",
175
+ id: "ai-check-formation-part",
176
+ title: "Part 1 — Word formation",
177
+ stimulus: {
178
+ id: "ai-check-formation-instructions",
179
+ kind: "text",
180
+ body: "Use the word in capitals at the end of each sentence to form a word that fits the gap."
181
+ },
182
+ items: [formation]
183
+ };
98
184
  const weekend = {
99
185
  schemaVersion: "1.0",
100
186
  type: "written-response",
@@ -122,6 +208,117 @@ const weekend = {
122
208
  }
123
209
  ] }
124
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
+ };
125
322
  const weather = {
126
323
  schemaVersion: "1.0",
127
324
  type: "read-aloud",
@@ -364,10 +561,11 @@ const READ_ALOUD_SETTINGS = {
364
561
  }] }
365
562
  };
366
563
  /** Builds a case, failing loudly rather than shipping a case with no request. */
367
- function explanationCase(id, about, data, response) {
564
+ function explanationCase(id, about, data, response, group) {
368
565
  const request = require_ai_writing.aiExplanationRequest({
369
566
  data,
370
- response
567
+ response,
568
+ ...group ? { group } : {}
371
569
  });
372
570
  if (request === null) throw new Error(`ai-check: no explanation request for "${id}"`);
373
571
  return {
@@ -377,11 +575,12 @@ function explanationCase(id, about, data, response) {
377
575
  request
378
576
  };
379
577
  }
380
- function hintCase(id, about, data, response, previousHints = []) {
578
+ function hintCase(id, about, data, response, previousHints = [], group) {
381
579
  const request = require_ai_writing.aiHintRequest({
382
580
  data,
383
581
  response,
384
- previousHints
582
+ previousHints,
583
+ ...group ? { group } : {}
385
584
  });
386
585
  if (request === null) throw new Error(`ai-check: no hint request for "${id}"`);
387
586
  return {
@@ -391,11 +590,12 @@ function hintCase(id, about, data, response, previousHints = []) {
391
590
  request
392
591
  };
393
592
  }
394
- function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
593
+ function writingCase(id, about, text, previousFeedback = [], learnerLocale, task = { data: weekend }) {
395
594
  const request = require_ai_writing.aiWritingFeedbackRequest({
396
- data: weekend,
595
+ data: task.data,
397
596
  response: drafted(text),
398
597
  previousFeedback,
598
+ ...task.group ? { group: task.group } : {},
399
599
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
400
600
  });
401
601
  if (request === null) throw new Error(`ai-check: no writing feedback request for "${id}"`);
@@ -406,6 +606,21 @@ function writingCase(id, about, text, previousFeedback = [], learnerLocale) {
406
606
  request
407
607
  };
408
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
+ }
409
624
  function coachingCase(id, about, marks, learnerLocale) {
410
625
  const assessment = marks.assessment ?? forecast(true);
411
626
  const grade = require_scoring.gradeReadAloud(weather, {
@@ -442,8 +657,9 @@ function coachingCase(id, about, marks, learnerLocale) {
442
657
  /**
443
658
  * The cases {@link runAiCheck} runs: every type the SDK explains, answered
444
659
  * right, wrong and — where a type can be — partly right; every type it hints
445
- * for, before an answer and after a wrong one; and drafts of an essay, with
446
- * 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.
447
663
  *
448
664
  * They are ordinary calls. What makes them a test is that the SDK knows the
449
665
  * answer to each, so it can check what your model writes about it.
@@ -478,6 +694,7 @@ function aiCheckCases() {
478
694
  })),
479
695
  explanationCase("dictation-near", "Typed the sentence with two words wrong", listening, wrote("The train leaves at half past five")),
480
696
  explanationCase("dictation-correct", "Typed the sentence correctly", listening, wrote("The train leaves at half past nine.")),
697
+ explanationCase("mc-reading-wrong", "Chose 'Only at full moon', on a question about its group's passage", springTides, chose(["full-moon"]), tides),
481
698
  hintCase("mc-hint-1", "Has not answered yet", capital, chose([])),
482
699
  hintCase("mc-hint-2", "Asked again after choosing Seville", capital, chose(["seville"]), ["Think about where the government sits."]),
483
700
  hintCase("fib-hint-1", "Both blanks empty", introduction, typed({})),
@@ -487,10 +704,16 @@ function aiCheckCases() {
487
704
  }), ["Look again at the first blank."]),
488
705
  hintCase("gap-hint-1", "Neither gap chosen, in Spanish", landscape, picked({})),
489
706
  hintCase("gap-hint-2", "Asked again with the first gap wrong", landscape, picked({ g1: "campo" }), ["Piensa en la forma del terreno."]),
707
+ hintCase("mc-reading-hint", "Has not answered a question whose answer its group's passage prints", springTides, chose([]), [], tides),
708
+ hintCase("fib-formation-hint", "A word-formation blank, under the instructions its group gives", formation, typed({}), [], formationPart),
490
709
  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."),
491
710
  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."),
492
711
  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."]),
493
712
  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"),
713
+ 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, {
714
+ data: tidesSummary,
715
+ group: tides
716
+ }),
494
717
  coachingCase("coaching-slips", "A reading with a mispronounced \"th\" and a word left out", { assessment: forecast(true) }),
495
718
  coachingCase("coaching-clean", "A reading the engine marked correct throughout", { assessment: forecast(false) }),
496
719
  coachingCase("coaching-from-grade", "The same slips, from a stored grade: marks without sounds", { fromGradeOnly: true }),
@@ -559,7 +782,15 @@ function aiCheckCases() {
559
782
  locale: "en",
560
783
  instructions: "Focus on what Maria and her grandmother buy.",
561
784
  settings: { "read-aloud": READ_ALOUD_SETTINGS }
562
- })
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" })
563
794
  ];
564
795
  }
565
796
  //#endregion
@@ -588,6 +819,7 @@ function aiCheckCases() {
588
819
  * pronunciationCoaching: (request) => callMyModel(COACHING_PROMPT, request),
589
820
  * critique: (request) => callMyModel(CRITIQUE_PROMPT, request),
590
821
  * drafts: (request) => callMyModel(DRAFTS_PROMPT, request),
822
+ * grading: (request) => callMyModel(GRADING_PROMPT, request),
591
823
  * });
592
824
  * console.log(formatAiCheckReport(report));
593
825
  * if (report.refused > 0 || report.errors > 0) {
@@ -600,18 +832,27 @@ function aiCheckCases() {
600
832
  * to take most seriously — that prompt gives answers away. Next come
601
833
  * `misquotes-answer` on a writing case — that prompt corrects words the learner
602
834
  * never wrote — and `contradicts-marks` on a coaching case: that prompt coaches
603
- * 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.
604
843
  */
605
- const REFUSALS = [
606
- "malformed",
607
- "empty",
608
- "too-long",
609
- "contradicts-grade",
610
- "reveals-answer",
611
- "misquotes-answer",
612
- "contradicts-marks",
613
- "contradicts-item"
614
- ];
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
+ });
615
856
  /** The port a case is for. */
616
857
  function portFor(ports, one) {
617
858
  switch (one.request.feature) {
@@ -620,6 +861,7 @@ function portFor(ports, one) {
620
861
  case "pronunciation-coaching": return ports.pronunciationCoaching;
621
862
  case "item-critique": return ports.critique;
622
863
  case "draft-generation": return ports.drafts;
864
+ case "grading": return ports.grading;
623
865
  default: return ports.writingFeedback;
624
866
  }
625
867
  }
@@ -642,6 +884,15 @@ async function runCase(ports, one) {
642
884
  };
643
885
  }
644
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
+ }
645
896
  if (one.request.feature === "draft-generation") {
646
897
  let next = 0;
647
898
  const checked = require_ai_writing.checkAiDrafts(raw, one.request, { newId: () => {