@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
@@ -1,6 +1,95 @@
1
- const require_registry = require("./registry-DVi0LR_i.cjs");
2
- const require_scoring = require("./scoring-B2_HEhMT.cjs");
3
- const require_item_group = require("./item-group-DroFXgLb.cjs");
1
+ const require_registry = require("./registry-Ce00A_OW.cjs");
2
+ const require_scoring = require("./scoring-lKQVYsxy.cjs");
3
+ const require_item_group = require("./item-group-DEB7nLID.cjs");
4
+ //#region src/ai-material.ts
5
+ /**
6
+ * What a model is told about the group a question sits in: the material the
7
+ * learner reads and sees beside it.
8
+ *
9
+ * Internal: the request builders in `ai.ts` and `ai-writing.ts` are its public
10
+ * face, through their `group` input.
11
+ */
12
+ /**
13
+ * The longest material text a model is given, in code points. A longer one is
14
+ * cut and marked `truncated`: a long reading passage on an exam paper runs to
15
+ * 4,000–7,000.
16
+ */
17
+ const AI_MATERIAL_MAX_LENGTH = 12e3;
18
+ const KINDS$2 = {
19
+ text: true,
20
+ audio: true,
21
+ video: true,
22
+ image: true,
23
+ mixed: true
24
+ };
25
+ const MEDIA_TYPES = {
26
+ image: true,
27
+ audio: true,
28
+ video: true,
29
+ embed: true
30
+ };
31
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
32
+ /** A string with something in it, as written; otherwise nothing. */
33
+ const filled = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
34
+ /**
35
+ * `text` cut to {@link AI_MATERIAL_MAX_LENGTH} code points. The cut falls at
36
+ * the last space in its second half, so no word is broken; in a script written
37
+ * without spaces, at the limit itself.
38
+ */
39
+ function cut(text) {
40
+ const points = [...text];
41
+ if (points.length <= 12e3) return {
42
+ text,
43
+ truncated: false
44
+ };
45
+ let end = AI_MATERIAL_MAX_LENGTH;
46
+ for (let at = AI_MATERIAL_MAX_LENGTH; at >= AI_MATERIAL_MAX_LENGTH / 2; at -= 1) if (/\s/u.test(points[at])) {
47
+ end = at;
48
+ break;
49
+ }
50
+ return {
51
+ text: points.slice(0, end).join("").trimEnd(),
52
+ truncated: true
53
+ };
54
+ }
55
+ /**
56
+ * The facts of `group`'s material, or `null` without a group or a stimulus it
57
+ * can read: not an object, or of a kind the SDK does not know.
58
+ *
59
+ * Only what the learner sees reaches a model. The `transcript` is author-only
60
+ * and never read, so the facts are the same for a whole group and a redacted
61
+ * one; a media address is never read either, since a model cannot open one
62
+ * and a signed address must not end up in a prompt. Each field is read once.
63
+ */
64
+ function materialFacts(group) {
65
+ if (!isRecord$4(group)) return null;
66
+ const stimulus = group.stimulus;
67
+ if (!isRecord$4(stimulus)) return null;
68
+ const kind = stimulus.kind;
69
+ if (typeof kind !== "string" || !Object.hasOwn(KINDS$2, kind)) return null;
70
+ const title = filled(stimulus.title) ?? filled(group.title);
71
+ const body = filled(stimulus.body);
72
+ const text = body === void 0 ? void 0 : cut(body);
73
+ const rawMedia = stimulus.media;
74
+ const mediaType = isRecord$4(rawMedia) ? rawMedia.type : void 0;
75
+ const alt = isRecord$4(rawMedia) ? filled(rawMedia.alt) : void 0;
76
+ const media = typeof mediaType === "string" && Object.hasOwn(MEDIA_TYPES, mediaType) ? {
77
+ type: mediaType,
78
+ ...alt !== void 0 ? { alt } : {}
79
+ } : void 0;
80
+ const attribution = filled(stimulus.attribution);
81
+ const locale = filled(stimulus.locale);
82
+ return {
83
+ kind,
84
+ ...title !== void 0 ? { title } : {},
85
+ ...text !== void 0 ? { text: text.text } : {},
86
+ ...text?.truncated === true ? { truncated: true } : {},
87
+ ...media !== void 0 ? { media } : {},
88
+ ...attribution !== void 0 ? { attribution } : {},
89
+ ...locale !== void 0 ? { locale } : {}
90
+ };
91
+ }
92
+ //#endregion
4
93
  //#region src/ai-text.ts
5
94
  /**
6
95
  * How the SDK reads text a model wrote, before a learner does. Internal: shared
@@ -341,6 +430,10 @@ function gradeLocally(data, response) {
341
430
  * positive and finite, `score` finite from 0 to it) gets no explanation at all:
342
431
  * explaining 85 "out of 1" would tell the model a wrong answer was correct, and
343
432
  * grading locally instead would speak to a grade the screen does not show.
433
+ *
434
+ * `group` is the item group the question sits in: its material — the passage,
435
+ * or the instructions the group's questions share — goes with the request as
436
+ * `material`, as the learner sees it.
344
437
  */
345
438
  function aiExplanationRequest(input) {
346
439
  const { data, response, outcome, learnerLocale } = input;
@@ -366,10 +459,12 @@ function aiExplanationRequest(input) {
366
459
  if (grade === null) return null;
367
460
  const facts = buildAiFacts(data, response, details);
368
461
  if (facts === null) return null;
462
+ const material = materialFacts(input.group);
369
463
  return {
370
464
  feature: "explanation",
371
465
  facts,
372
466
  grade,
467
+ ...material !== null ? { material } : {},
373
468
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
374
469
  };
375
470
  }
@@ -380,7 +475,11 @@ function aiExplanationRequest(input) {
380
475
  *
381
476
  * The facts carry the key, and which parts the learner has right so far, so a
382
477
  * model can hint where it helps. The learner never sees them; a hint that
383
- * contains an answer is refused by {@link checkAiHint}.
478
+ * contains an answer is refused by {@link checkAiHint}, whatever it quotes —
479
+ * the group's material included.
480
+ *
481
+ * `group` is the item group the question sits in, as for
482
+ * {@link aiExplanationRequest}.
384
483
  */
385
484
  function aiHintRequest(input) {
386
485
  const { data, previousHints, learnerLocale } = input;
@@ -388,11 +487,13 @@ function aiHintRequest(input) {
388
487
  const response = input.response ?? emptyResponse(data.type);
389
488
  const facts = buildAiFacts(data, response, (response === null ? null : gradeLocally(data, response))?.details ?? null);
390
489
  if (facts === null) return null;
490
+ const material = materialFacts(input.group);
391
491
  return {
392
492
  feature: "hint",
393
493
  facts,
394
494
  hintNumber: previousHints.length + 1,
395
495
  previousHints: [...previousHints],
496
+ ...material !== null ? { material } : {},
396
497
  ...learnerLocale !== void 0 ? { learnerLocale } : {}
397
498
  };
398
499
  }
@@ -2188,61 +2289,19 @@ function interactiveVideoFromDrafts(input) {
2188
2289
  };
2189
2290
  }
2190
2291
  //#endregion
2191
- //#region src/ai-writing.ts
2292
+ //#region src/ai-written.ts
2293
+ /**
2294
+ * What the AI features about a written response share: reading a model's text
2295
+ * fields, and finding each correction's quote in the learner's text.
2296
+ *
2297
+ * Internal. `ai-writing.ts` (feedback on a draft) and `ai-grading.ts` (a
2298
+ * grade) are their public faces, and hold every correction to the same
2299
+ * anchoring.
2300
+ */
2192
2301
  /** The most corrections one piece of writing feedback may carry. More is refused, not cut. */
2193
2302
  const AI_WRITING_MAX_CORRECTIONS = 20;
2194
2303
  /** The longest quote, correction, explanation or criterion comment, in code points. */
2195
2304
  const AI_WRITING_MAX_FIELD_LENGTH = 500;
2196
- /** The longest `category` or `band`: a tag, not a sentence. */
2197
- const MAX_TAG_LENGTH = 40;
2198
- /**
2199
- * The request for feedback on a draft, or `null` when there is none to give:
2200
- * not a written response, an author who switched explanations off (writing
2201
- * feedback reads `ai.explanations`), a redacted item — feedback is for
2202
- * practice, where the item is whole — or a draft with nothing in it.
2203
- *
2204
- * The facts are the task as the learner saw it, the draft verbatim with its
2205
- * word count recomputed as the grader will count it, and the rubric the answer
2206
- * will be graded on. `previousFeedback` is the text of the feedback already
2207
- * shown on earlier drafts, so a model can speak to what changed.
2208
- */
2209
- function aiWritingFeedbackRequest(input) {
2210
- const { data, response, learnerLocale } = input;
2211
- if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
2212
- const text = response?.type === "written-response" ? response.text : "";
2213
- if (typeof text !== "string" || text.trim() === "") return null;
2214
- const item = data;
2215
- const wordCount = require_registry.countWords(text);
2216
- const minWords = typeof item.minWords === "number" ? item.minWords : 0;
2217
- const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
2218
- const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
2219
- const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
2220
- name: criterion.name,
2221
- ...typeof criterion.description === "string" ? { description: criterion.description } : {},
2222
- weight: criterion.weight
2223
- }));
2224
- const previousFeedback = [...input.previousFeedback ?? []];
2225
- return {
2226
- feature: "writing-feedback",
2227
- facts: {
2228
- activityType: "written-response",
2229
- activityId: data.id,
2230
- title: data.title,
2231
- ...typeof data.locale === "string" ? { locale: data.locale } : {},
2232
- prompt: typeof item.prompt === "string" ? item.prompt : "",
2233
- text,
2234
- wordCount,
2235
- minWords,
2236
- maxWords,
2237
- withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
2238
- rubric,
2239
- ...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
2240
- },
2241
- draftNumber: previousFeedback.length + 1,
2242
- previousFeedback,
2243
- ...learnerLocale !== void 0 ? { learnerLocale } : {}
2244
- };
2245
- }
2246
2305
  /**
2247
2306
  * Where `quote` sits in the draft: at `claimed`, when the model named a range —
2248
2307
  * which must then hold the quote — or else at its first occurrence not already
@@ -2341,7 +2400,7 @@ function checkCorrections(raw, text) {
2341
2400
  refusal: "malformed"
2342
2401
  };
2343
2402
  const explanation = textField(entry.explanation, 500);
2344
- const category = textField(entry.category, MAX_TAG_LENGTH);
2403
+ const category = textField(entry.category, 40);
2345
2404
  if (!explanation.ok) return explanation;
2346
2405
  if (!category.ok) return category;
2347
2406
  let claimed;
@@ -2373,6 +2432,385 @@ function checkCorrections(raw, text) {
2373
2432
  corrections
2374
2433
  };
2375
2434
  }
2435
+ //#endregion
2436
+ //#region src/ai-grading.ts
2437
+ /**
2438
+ * How far a model's level probabilities may sum from 1 and still be a
2439
+ * distribution: rounding in a model's output, not a different judgement.
2440
+ */
2441
+ const AI_GRADING_PROBABILITY_TOLERANCE = require_registry.LEVEL_PROBABILITY_TOLERANCE;
2442
+ /**
2443
+ * The request for a model's grade of a submitted written response, or why there
2444
+ * is none to make.
2445
+ *
2446
+ * Grading happens on the server, from the whole item: the rubric is what a
2447
+ * model is held to, so an item without one — or one whose criteria are not each
2448
+ * a name and a weight — gives no request, nor does a `redact()` projection or
2449
+ * an empty response. A criterion's `levels`, when there are any, must be ones
2450
+ * the grading functions can read; levels they cannot read give no request
2451
+ * rather than a grade on a scale the author did not mean. `group` is the item
2452
+ * group the task sits in: its material goes with the request as `material`, as
2453
+ * on feedback on writing.
2454
+ *
2455
+ * The author's AI switch (`ai.explanations`) is not read: grading is the
2456
+ * deployment's decision, made on the server, not help a learner asks for.
2457
+ */
2458
+ function aiGradingRequest(input) {
2459
+ const { data, response, learnerLocale } = input;
2460
+ if (data.type !== "written-response") return {
2461
+ ok: false,
2462
+ problem: "not-written-response"
2463
+ };
2464
+ if (data.redacted === true) return {
2465
+ ok: false,
2466
+ problem: "redacted"
2467
+ };
2468
+ const item = data;
2469
+ const rubric = rubricFacts(item.rubric);
2470
+ if (!rubric.ok) return rubric;
2471
+ const text = response?.type === "written-response" ? response.text : void 0;
2472
+ if (typeof text !== "string" || text.trim() === "") return {
2473
+ ok: false,
2474
+ problem: "empty-response"
2475
+ };
2476
+ const wordCount = require_registry.countWords(text);
2477
+ const minWords = typeof item.minWords === "number" ? item.minWords : 0;
2478
+ const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
2479
+ const material = materialFacts(input.group);
2480
+ return {
2481
+ ok: true,
2482
+ request: {
2483
+ feature: "grading",
2484
+ facts: {
2485
+ activityType: "written-response",
2486
+ activityId: data.id,
2487
+ title: data.title,
2488
+ ...typeof data.locale === "string" ? { locale: data.locale } : {},
2489
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
2490
+ text,
2491
+ wordCount,
2492
+ minWords,
2493
+ maxWords,
2494
+ withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
2495
+ rubric: rubric.criteria,
2496
+ ...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
2497
+ },
2498
+ ...material !== null ? { material } : {},
2499
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
2500
+ }
2501
+ };
2502
+ }
2503
+ /** The rubric as a model is given it, each criterion's levels read. */
2504
+ function rubricFacts(rubric) {
2505
+ const listed = isRecord(rubric) ? rubric.criteria : void 0;
2506
+ if (!Array.isArray(listed) || listed.length === 0) return {
2507
+ ok: false,
2508
+ problem: "no-rubric"
2509
+ };
2510
+ const criteria = [];
2511
+ const names = /* @__PURE__ */ new Set();
2512
+ for (const [index, criterion] of listed.entries()) {
2513
+ if (!isRecord(criterion)) return {
2514
+ ok: false,
2515
+ problem: "no-rubric",
2516
+ path: [
2517
+ "rubric",
2518
+ "criteria",
2519
+ index
2520
+ ]
2521
+ };
2522
+ const { name, description, weight } = criterion;
2523
+ if (typeof name !== "string" || name.trim() === "" || typeof weight !== "number") return {
2524
+ ok: false,
2525
+ problem: "no-rubric",
2526
+ path: [
2527
+ "rubric",
2528
+ "criteria",
2529
+ index
2530
+ ]
2531
+ };
2532
+ if (names.has(require_registry.levelKey(name))) return {
2533
+ ok: false,
2534
+ problem: "duplicate-criteria",
2535
+ path: [
2536
+ "rubric",
2537
+ "criteria",
2538
+ index,
2539
+ "name"
2540
+ ]
2541
+ };
2542
+ names.add(require_registry.levelKey(name));
2543
+ const levels = require_registry.readRubricLevels(criterion.levels);
2544
+ if (!levels.ok) return {
2545
+ ok: false,
2546
+ problem: "unreadable-levels",
2547
+ path: [
2548
+ "rubric",
2549
+ "criteria",
2550
+ index,
2551
+ "levels"
2552
+ ]
2553
+ };
2554
+ criteria.push({
2555
+ name,
2556
+ ...typeof description === "string" ? { description } : {},
2557
+ weight,
2558
+ levels: levels.levels
2559
+ });
2560
+ }
2561
+ return {
2562
+ ok: true,
2563
+ criteria
2564
+ };
2565
+ }
2566
+ /**
2567
+ * The rubric's criteria by name, folded as level labels are: composed, trimmed,
2568
+ * spaced once, lower-cased. A model that writes "task achievement" for "Task
2569
+ * achievement" judged that criterion. The request refused a rubric with two
2570
+ * names alike once folded, so a folded name is one criterion's alone.
2571
+ */
2572
+ function criterionIndex(rubric) {
2573
+ const folded = new Map(rubric.map((criterion, index) => [require_registry.levelKey(criterion.name), index]));
2574
+ return (name) => folded.get(require_registry.levelKey(name));
2575
+ }
2576
+ /** A finite number. */
2577
+ const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
2578
+ /**
2579
+ * One criterion's judgement, on that criterion's scale: a level it has, with
2580
+ * any probabilities a distribution over its levels whose likeliest is the
2581
+ * level chosen; or, without levels, a score from 0 to its `maxScore` (1 by
2582
+ * default).
2583
+ */
2584
+ function judgeCriterion(entry, criterion) {
2585
+ const { level, score, maxScore, probabilities } = entry;
2586
+ const levels = criterion.levels;
2587
+ if (levels === null) {
2588
+ if (level !== void 0 || probabilities !== void 0) return {
2589
+ ok: false,
2590
+ refusal: "out-of-scale"
2591
+ };
2592
+ if (score === void 0) return {
2593
+ ok: false,
2594
+ refusal: "misses-criteria"
2595
+ };
2596
+ if (!isNumber(score) || maxScore !== void 0 && !isNumber(maxScore)) return {
2597
+ ok: false,
2598
+ refusal: "malformed"
2599
+ };
2600
+ const max = maxScore ?? 1;
2601
+ if (!(max > 0) || score < 0 || score - max > max * 1e-9) return {
2602
+ ok: false,
2603
+ refusal: "out-of-scale"
2604
+ };
2605
+ return {
2606
+ ok: true,
2607
+ judged: {
2608
+ name: criterion.name,
2609
+ score: Math.min(1, score / max)
2610
+ }
2611
+ };
2612
+ }
2613
+ if (score !== void 0 || maxScore !== void 0) return {
2614
+ ok: false,
2615
+ refusal: "out-of-scale"
2616
+ };
2617
+ if (level === void 0) return {
2618
+ ok: false,
2619
+ refusal: "misses-criteria"
2620
+ };
2621
+ if (typeof level !== "string") return {
2622
+ ok: false,
2623
+ refusal: "malformed"
2624
+ };
2625
+ const chosen = levels.findIndex((one) => require_registry.levelKey(one.label) === require_registry.levelKey(level));
2626
+ if (chosen === -1) return {
2627
+ ok: false,
2628
+ refusal: "out-of-scale"
2629
+ };
2630
+ const judged = {
2631
+ name: criterion.name,
2632
+ level: levels[chosen].label,
2633
+ score: chosen / (levels.length - 1)
2634
+ };
2635
+ if (probabilities === void 0) return {
2636
+ ok: true,
2637
+ judged
2638
+ };
2639
+ if (!Array.isArray(probabilities)) return {
2640
+ ok: false,
2641
+ refusal: "malformed"
2642
+ };
2643
+ const values = require_registry.levelDistribution(probabilities, levels.length, chosen);
2644
+ if (values === null) return {
2645
+ ok: false,
2646
+ refusal: "out-of-scale"
2647
+ };
2648
+ return {
2649
+ ok: true,
2650
+ judged: {
2651
+ ...judged,
2652
+ probabilities: values
2653
+ }
2654
+ };
2655
+ }
2656
+ /**
2657
+ * Checks a grade a port returned for `request`. Whole or nothing, as every AI
2658
+ * text is: a reply the SDK doubts in any part is refused, and is no grade —
2659
+ * re-ask the model or send the response to a person; never record a 0.
2660
+ *
2661
+ * - **Every criterion of the rubric, once** (`misses-criteria`). Names match as
2662
+ * written, or folded when the folded name is one criterion's alone. Leaving
2663
+ * one out, inventing or renaming one, judging one twice or calling one not
2664
+ * applicable refuses the reply: a total renormalised over fewer criteria
2665
+ * is a grade of a different task.
2666
+ * - **On each criterion's own scale** (`out-of-scale`). With levels: a `level`
2667
+ * the criterion has, as labelled (case and spacing aside), and any
2668
+ * `probabilities` a distribution over its levels, lowest first, whose
2669
+ * likeliest is the level chosen. Without levels: a `score` from 0 to its
2670
+ * `maxScore`, 1 by default.
2671
+ * - **Every correction quotes the response** (`misquotes-answer`), anchored as
2672
+ * feedback on writing anchors it.
2673
+ * - **Text fields** — a criterion's `comment` (at most
2674
+ * `AI_WRITING_MAX_FIELD_LENGTH` code points), `feedback` and `rationale`
2675
+ * (at most `AI_TEXT_MAX_LENGTH`) — are plain text, cleaned; longer is
2676
+ * `too-long`. `requiresHumanReview` is a boolean.
2677
+ *
2678
+ * The total is not computed here: `gradeFromJudgement` computes it, as
2679
+ * `gradeFromRubric` computes every grade in the SDK.
2680
+ */
2681
+ function checkAiGrading(raw, request) {
2682
+ if (!isRecord(raw)) return {
2683
+ ok: false,
2684
+ refusal: "malformed"
2685
+ };
2686
+ const { criteria: listed, feedback, corrections, rationale, requiresHumanReview } = raw;
2687
+ if (listed === void 0) return {
2688
+ ok: false,
2689
+ refusal: "misses-criteria"
2690
+ };
2691
+ if (!Array.isArray(listed)) return {
2692
+ ok: false,
2693
+ refusal: "malformed"
2694
+ };
2695
+ const rubric = request.facts.rubric;
2696
+ const indexOf = criterionIndex(rubric);
2697
+ const judged = rubric.map(() => void 0);
2698
+ for (const entry of listed) {
2699
+ if (!isRecord(entry)) return {
2700
+ ok: false,
2701
+ refusal: "malformed"
2702
+ };
2703
+ const name = entry.name;
2704
+ const notApplicable = entry.notApplicable;
2705
+ const written = entry.comment;
2706
+ const scale = {
2707
+ level: entry.level,
2708
+ score: entry.score,
2709
+ maxScore: entry.maxScore,
2710
+ probabilities: entry.probabilities
2711
+ };
2712
+ if (typeof name !== "string" || notApplicable !== void 0 && typeof notApplicable !== "boolean") return {
2713
+ ok: false,
2714
+ refusal: "malformed"
2715
+ };
2716
+ const index = indexOf(name);
2717
+ if (index === void 0 || judged[index] !== void 0 || notApplicable === true) return {
2718
+ ok: false,
2719
+ refusal: "misses-criteria"
2720
+ };
2721
+ const criterion = rubric[index];
2722
+ const scaled = judgeCriterion(scale, criterion);
2723
+ if (!scaled.ok) return scaled;
2724
+ const comment = textField(written, 500);
2725
+ if (!comment.ok) return comment;
2726
+ judged[index] = {
2727
+ ...scaled.judged,
2728
+ ...comment.value !== void 0 ? { comment: comment.value } : {}
2729
+ };
2730
+ }
2731
+ if (judged.some((one) => one === void 0)) return {
2732
+ ok: false,
2733
+ refusal: "misses-criteria"
2734
+ };
2735
+ const anchored = checkCorrections(corrections, request.facts.text);
2736
+ if (!anchored.ok) return anchored;
2737
+ const said = textField(feedback, AI_TEXT_MAX_LENGTH);
2738
+ if (!said.ok) return said;
2739
+ const why = textField(rationale, AI_TEXT_MAX_LENGTH);
2740
+ if (!why.ok) return why;
2741
+ if (requiresHumanReview !== void 0 && typeof requiresHumanReview !== "boolean") return {
2742
+ ok: false,
2743
+ refusal: "malformed"
2744
+ };
2745
+ const provenance = provenanceOf(raw.provenance);
2746
+ const usage = usageOf(raw.usage);
2747
+ return {
2748
+ ok: true,
2749
+ judgement: {
2750
+ criteria: judged,
2751
+ feedback: said.value ?? null,
2752
+ corrections: anchored.corrections,
2753
+ ...why.value !== void 0 ? { rationale: why.value } : {},
2754
+ requiresHumanReview: requiresHumanReview === true,
2755
+ ...provenance !== void 0 ? { provenance } : {},
2756
+ ...usage !== void 0 ? { usage } : {}
2757
+ }
2758
+ };
2759
+ }
2760
+ //#endregion
2761
+ //#region src/ai-writing.ts
2762
+ /**
2763
+ * The request for feedback on a draft, or `null` when there is none to give:
2764
+ * not a written response, an author who switched explanations off (writing
2765
+ * feedback reads `ai.explanations`), a redacted item — feedback is for
2766
+ * practice, where the item is whole — or a draft with nothing in it.
2767
+ *
2768
+ * The facts are the task as the learner saw it, the draft verbatim with its
2769
+ * word count recomputed as the grader will count it, and the rubric the answer
2770
+ * will be graded on. `previousFeedback` is the text of the feedback already
2771
+ * shown on earlier drafts, so a model can speak to what changed. `group` is the
2772
+ * item group the task sits in: its material, the text a summary is of, goes
2773
+ * with the request as `material`.
2774
+ */
2775
+ function aiWritingFeedbackRequest(input) {
2776
+ const { data, response, learnerLocale } = input;
2777
+ if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
2778
+ const text = response?.type === "written-response" ? response.text : "";
2779
+ if (typeof text !== "string" || text.trim() === "") return null;
2780
+ const item = data;
2781
+ const wordCount = require_registry.countWords(text);
2782
+ const minWords = typeof item.minWords === "number" ? item.minWords : 0;
2783
+ const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
2784
+ const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
2785
+ const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
2786
+ name: criterion.name,
2787
+ ...typeof criterion.description === "string" ? { description: criterion.description } : {},
2788
+ weight: criterion.weight
2789
+ }));
2790
+ const previousFeedback = [...input.previousFeedback ?? []];
2791
+ const material = materialFacts(input.group);
2792
+ return {
2793
+ feature: "writing-feedback",
2794
+ facts: {
2795
+ activityType: "written-response",
2796
+ activityId: data.id,
2797
+ title: data.title,
2798
+ ...typeof data.locale === "string" ? { locale: data.locale } : {},
2799
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
2800
+ text,
2801
+ wordCount,
2802
+ minWords,
2803
+ maxWords,
2804
+ withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
2805
+ rubric,
2806
+ ...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
2807
+ },
2808
+ draftNumber: previousFeedback.length + 1,
2809
+ previousFeedback,
2810
+ ...material !== null ? { material } : {},
2811
+ ...learnerLocale !== void 0 ? { learnerLocale } : {}
2812
+ };
2813
+ }
2376
2814
  /** A criterion score a grade could be computed from: finite, 0 or more, and not above its maximum. */
2377
2815
  function isJudgedScore(score, maxScore) {
2378
2816
  if (score === void 0) return maxScore === void 0 || typeof maxScore === "number" && maxScore > 0;
@@ -2419,7 +2857,7 @@ function checkCriteria(raw, rubric) {
2419
2857
  ok: false,
2420
2858
  refusal: "malformed"
2421
2859
  };
2422
- const band = textField(entry.band, MAX_TAG_LENGTH);
2860
+ const band = textField(entry.band, 40);
2423
2861
  const comment = textField(entry.comment, 500);
2424
2862
  if (!band.ok) return band;
2425
2863
  if (!comment.ok) return comment;
@@ -2533,12 +2971,24 @@ Object.defineProperty(exports, "AI_EXPLANATION_TYPES", {
2533
2971
  return AI_EXPLANATION_TYPES;
2534
2972
  }
2535
2973
  });
2974
+ Object.defineProperty(exports, "AI_GRADING_PROBABILITY_TOLERANCE", {
2975
+ enumerable: true,
2976
+ get: function() {
2977
+ return AI_GRADING_PROBABILITY_TOLERANCE;
2978
+ }
2979
+ });
2536
2980
  Object.defineProperty(exports, "AI_HINT_TYPES", {
2537
2981
  enumerable: true,
2538
2982
  get: function() {
2539
2983
  return AI_HINT_TYPES;
2540
2984
  }
2541
2985
  });
2986
+ Object.defineProperty(exports, "AI_MATERIAL_MAX_LENGTH", {
2987
+ enumerable: true,
2988
+ get: function() {
2989
+ return AI_MATERIAL_MAX_LENGTH;
2990
+ }
2991
+ });
2542
2992
  Object.defineProperty(exports, "AI_TEXT_MAX_LENGTH", {
2543
2993
  enumerable: true,
2544
2994
  get: function() {
@@ -2599,6 +3049,12 @@ Object.defineProperty(exports, "aiGradeOf", {
2599
3049
  return aiGradeOf;
2600
3050
  }
2601
3051
  });
3052
+ Object.defineProperty(exports, "aiGradingRequest", {
3053
+ enumerable: true,
3054
+ get: function() {
3055
+ return aiGradingRequest;
3056
+ }
3057
+ });
2602
3058
  Object.defineProperty(exports, "aiHintRequest", {
2603
3059
  enumerable: true,
2604
3060
  get: function() {
@@ -2647,6 +3103,12 @@ Object.defineProperty(exports, "checkAiExplanation", {
2647
3103
  return checkAiExplanation;
2648
3104
  }
2649
3105
  });
3106
+ Object.defineProperty(exports, "checkAiGrading", {
3107
+ enumerable: true,
3108
+ get: function() {
3109
+ return checkAiGrading;
3110
+ }
3111
+ });
2650
3112
  Object.defineProperty(exports, "checkAiHint", {
2651
3113
  enumerable: true,
2652
3114
  get: function() {
@@ -2732,4 +3194,4 @@ Object.defineProperty(exports, "validateItemGroupDraft", {
2732
3194
  }
2733
3195
  });
2734
3196
 
2735
- //# sourceMappingURL=ai-writing-DCVkv4Zj.cjs.map
3197
+ //# sourceMappingURL=ai-writing-Cen7-3qs.cjs.map