@intellectif/lk-core 1.4.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 +45 -0
  2. package/README.md +2 -0
  3. package/dist/{ai-CnmLPMzd.d.cts → ai-BYryFkF9.d.cts} +199 -5
  4. package/dist/ai-BYryFkF9.d.cts.map +1 -0
  5. package/dist/{ai-CnmLPMzd.d.ts → ai-BYryFkF9.d.ts} +199 -5
  6. package/dist/ai-BYryFkF9.d.ts.map +1 -0
  7. package/dist/ai-check.cjs +175 -17
  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 +175 -17
  14. package/dist/ai-check.js.map +1 -1
  15. package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-Cen7-3qs.cjs} +412 -61
  16. package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
  17. package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-D8PdhJJz.js} +395 -62
  18. package/dist/ai-writing-D8PdhJJz.js.map +1 -0
  19. package/dist/{index-BAfEHCnD.d.cts → index-Bg0aeWtM.d.cts} +327 -3
  20. package/dist/index-Bg0aeWtM.d.cts.map +1 -0
  21. package/dist/{index-2nigSf4N.d.cts → index-CxpiWedW.d.ts} +6 -2
  22. package/dist/index-CxpiWedW.d.ts.map +1 -0
  23. package/dist/{index-BqMXWuNC.d.cts → index-D7R3Ll8R.d.cts} +2 -2
  24. package/dist/{index-BqMXWuNC.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
  25. package/dist/{index-CVWvymVb.d.ts → index-DUaW1Tj5.d.ts} +327 -3
  26. package/dist/index-DUaW1Tj5.d.ts.map +1 -0
  27. package/dist/{index-B54baFUA.d.ts → index-qMbxed1B.d.cts} +6 -2
  28. package/dist/index-qMbxed1B.d.cts.map +1 -0
  29. package/dist/{index-CcHDTxIZ.d.ts → index-wMc1E_aM.d.ts} +2 -2
  30. package/dist/{index-CcHDTxIZ.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
  31. package/dist/index.cjs +31 -130
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +85 -95
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +85 -95
  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-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/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,4 +1,4 @@
1
- import { $ as dictationReferenceWords, C as SPEECH_ASSESSMENT_MAX_TEXT_LENGTH, J as RedactedScoringError, K as ActivitySchemaError, Q as alignSequences, X as STEP, Y as UnknownActivityTypeError, _ as ReadAloudDataSchema, at as codePointLength, j as countWords, ot as normalizeDictationText, q as DeferredScoringError, t as getActivityTypeDescriptor, v as CANONICAL_LOCALE_RE, w as SPEECH_ASSESSMENT_MAX_WORDS, yt as maxUnits } from "./registry-Dsx9mNsS.js";
1
+ import { $ as RedactedScoringError, F as readRubricLevels, I as countWords, N as levelDistribution, P as levelKey, Q as DeferredScoringError, T as SPEECH_ASSESSMENT_MAX_WORDS, Z as ActivitySchemaError, dt as normalizeDictationText, et as UnknownActivityTypeError, it as dictationReferenceWords, rt as alignSequences, t as getActivityTypeDescriptor, tt as STEP, ut as codePointLength, v as ReadAloudDataSchema, w as SPEECH_ASSESSMENT_MAX_TEXT_LENGTH, wt as maxUnits, y as CANONICAL_LOCALE_RE } from "./registry-DaZRpqjW.js";
2
2
  import * as z from "zod/v4";
3
3
  //#region src/scoring/grade-numbers.ts
4
4
  /**
@@ -358,6 +358,406 @@ function selectFeedback(activityData, passed) {
358
358
  return (passed ? feedback.correct : feedback.incorrect) ?? null;
359
359
  }
360
360
  //#endregion
361
+ //#region src/content-hash.ts
362
+ /**
363
+ * Content fingerprinting — "is this the same content the learner was served?"
364
+ *
365
+ * A recorded attempt outlives the content it was taken against. Papers get
366
+ * corrected, a typo is fixed, an option is reworded — and six months later a
367
+ * remark, an appeal, or a per-item analysis is run against content that is no
368
+ * longer what the learner saw. Nothing warns anybody, because the ids all still
369
+ * match. A fingerprint stored with the attempt turns that silent drift into a
370
+ * question somebody can answer.
371
+ */
372
+ /**
373
+ * Deterministic JSON: object keys sorted, arrays left in order, `undefined`
374
+ * omitted from objects (matching `JSON.stringify`) and rendered as `null`
375
+ * inside arrays.
376
+ *
377
+ * Key order is the whole point. Two objects that differ only in the order
378
+ * their keys happened to be written are the SAME content, and a fingerprint
379
+ * that disagreed would raise a false alarm every time a payload made a
380
+ * round-trip through a different serializer.
381
+ */
382
+ function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
383
+ if (value === null) return "null";
384
+ if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value === 0 ? 0 : value) : `"#${String(value)}"`;
385
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
386
+ if (typeof value === "bigint") return `"#${value.toString()}n"`;
387
+ if (typeof value === "object") {
388
+ if (seen.has(value)) throw new Error("canonicalJson: the value contains a circular reference and cannot be fingerprinted.");
389
+ seen.add(value);
390
+ try {
391
+ if (Array.isArray(value)) return `[${value.map((element) => element === void 0 ? "null" : canonicalJson(element, seen)).join(",")}]`;
392
+ const source = value;
393
+ const parts = [];
394
+ for (const key of Object.keys(source).sort()) {
395
+ const entry = source[key];
396
+ if (entry === void 0) continue;
397
+ parts.push(`${JSON.stringify(key)}:${canonicalJson(entry, seen)}`);
398
+ }
399
+ return `{${parts.join(",")}}`;
400
+ } finally {
401
+ seen.delete(value);
402
+ }
403
+ }
404
+ return "null";
405
+ }
406
+ const FNV_OFFSET = 14695981039346656037n;
407
+ const FNV_PRIME = 1099511628211n;
408
+ const MASK64 = (1n << 64n) - 1n;
409
+ /**
410
+ * FNV-1a over the UTF-8 bytes of `input`, as 16 lowercase hex digits.
411
+ *
412
+ * **This is a change-detection fingerprint, not a tamper-evident signature.**
413
+ * It is deterministic across runtimes and dependency-free, which is what makes
414
+ * it usable in a stored grade record — but an adversary who can edit content
415
+ * can also, with effort, preserve the fingerprint. If you need the stronger
416
+ * property, sign the plan with a key the content author does not hold; this
417
+ * exists to catch honest edits, which is what actually happens.
418
+ */
419
+ function fingerprint(input) {
420
+ const bytes = new TextEncoder().encode(input);
421
+ let hash = FNV_OFFSET;
422
+ for (const byte of bytes) hash = (hash ^ BigInt(byte)) * FNV_PRIME & MASK64;
423
+ return hash.toString(16).padStart(16, "0");
424
+ }
425
+ /**
426
+ * Fingerprints any JSON-serialisable value through {@link canonicalJson}, so
427
+ * the result depends on the content and not on how it was written.
428
+ */
429
+ function contentHash(value) {
430
+ return fingerprint(canonicalJson(value));
431
+ }
432
+ //#endregion
433
+ //#region src/scoring/assisted/calibration.ts
434
+ const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
435
+ /** Population variance. */
436
+ function variance(values) {
437
+ const m = mean(values);
438
+ return mean(values.map((value) => (value - m) ** 2));
439
+ }
440
+ /** Pearson's r; `null` with fewer than two pairs or no variance on a side. */
441
+ function pearson(xs, ys) {
442
+ if (xs.length < 2) return null;
443
+ const mx = mean(xs);
444
+ const my = mean(ys);
445
+ let cov = 0;
446
+ let vx = 0;
447
+ let vy = 0;
448
+ for (let i = 0; i < xs.length; i += 1) {
449
+ const dx = xs[i] - mx;
450
+ const dy = ys[i] - my;
451
+ cov += dx * dy;
452
+ vx += dx * dx;
453
+ vy += dy * dy;
454
+ }
455
+ return vx === 0 || vy === 0 ? null : cov / Math.sqrt(vx * vy);
456
+ }
457
+ /** Model mean minus person mean, over the pooled population standard deviation. */
458
+ function standardizedMeanDifference(model, person) {
459
+ if (model.length === 0) return null;
460
+ const pooled = Math.sqrt((variance(model) + variance(person)) / 2);
461
+ return pooled === 0 ? null : (mean(model) - mean(person)) / pooled;
462
+ }
463
+ /** Cohen's kappa with quadratic weights over `k` levels; `null` when undefined. */
464
+ function quadraticKappa(rows, cols, k) {
465
+ const n = rows.length;
466
+ if (n === 0 || k < 2) return null;
467
+ const observed = Array.from({ length: k }, () => new Array(k).fill(0));
468
+ const rowTotals = new Array(k).fill(0);
469
+ const colTotals = new Array(k).fill(0);
470
+ for (let i = 0; i < n; i += 1) {
471
+ const r = rows[i];
472
+ const c = cols[i];
473
+ observed[r][c] = observed[r][c] + 1;
474
+ rowTotals[r] = rowTotals[r] + 1;
475
+ colTotals[c] = colTotals[c] + 1;
476
+ }
477
+ let disagreement = 0;
478
+ let chance = 0;
479
+ for (let r = 0; r < k; r += 1) for (let c = 0; c < k; c += 1) {
480
+ const weight = (r - c) ** 2 / (k - 1) ** 2;
481
+ disagreement += weight * observed[r][c];
482
+ chance += weight * rowTotals[r] * colTotals[c] / n;
483
+ }
484
+ return chance === 0 ? null : 1 - disagreement / chance;
485
+ }
486
+ /** Ten equal bins of confidence; the mean absolute gap between confidence and accuracy, weighted by bin size. */
487
+ function expectedCalibrationError(confidence, correct) {
488
+ const bins = Array.from({ length: 10 }, () => ({
489
+ n: 0,
490
+ confidence: 0,
491
+ correct: 0
492
+ }));
493
+ for (let i = 0; i < confidence.length; i += 1) {
494
+ const c = confidence[i];
495
+ const bin = bins[Math.min(9, Math.floor(c * 10))];
496
+ bin.n += 1;
497
+ bin.confidence += c;
498
+ bin.correct += correct[i] ? 1 : 0;
499
+ }
500
+ return bins.reduce((sum, bin) => bin.n === 0 ? sum : sum + bin.n / confidence.length * Math.abs(bin.correct / bin.n - bin.confidence / bin.n), 0);
501
+ }
502
+ /** The grader configuration a model grade comes from, on items with this rubric and target; `null` when the grade does not name its model and prompt. */
503
+ function graderConfiguration(grade, scope) {
504
+ const model = grade.grader?.model;
505
+ const promptHash = grade.grader?.promptHash;
506
+ if (typeof model !== "string" || model === "" || typeof promptHash !== "string" || promptHash === "") return null;
507
+ return {
508
+ model,
509
+ promptHash,
510
+ rubricHash: contentHash(scope.rubric ?? null),
511
+ languageTarget: typeof scope.languageTarget === "string" ? scope.languageTarget : null
512
+ };
513
+ }
514
+ /** Configurations as equal as a calibration requires: every field. */
515
+ function sameConfiguration(a, b) {
516
+ return a.model === b.model && a.promptHash === b.promptHash && a.rubricHash === b.rubricHash && a.languageTarget === b.languageTarget;
517
+ }
518
+ /** Each rubric criterion's scale, or the name of the first whose levels cannot be read. */
519
+ function rubricScales(rubric) {
520
+ const scales = [];
521
+ for (const criterion of rubric?.criteria ?? []) {
522
+ const read = readRubricLevels(criterion.levels);
523
+ if (!read.ok) return {
524
+ ok: false,
525
+ name: criterion.name
526
+ };
527
+ scales.push({
528
+ name: criterion.name,
529
+ levels: read.levels
530
+ });
531
+ }
532
+ return {
533
+ ok: true,
534
+ scales
535
+ };
536
+ }
537
+ /** A grade's judgement of the criterion named `name`, its name compared as labels are. */
538
+ function criterionOf(grade, name) {
539
+ return (grade.criteria ?? []).find((one) => levelKey(one.name) === levelKey(name));
540
+ }
541
+ /**
542
+ * A judgement on a criterion's levels: the index of the level its `band`
543
+ * names, and its `levelProbabilities` when they are a distribution over the
544
+ * levels whose likeliest is that level. `null` when it names no level the
545
+ * criterion has.
546
+ */
547
+ function levelOf(judged, levels) {
548
+ const band = judged.band;
549
+ const level = typeof band === "string" ? levels.findIndex((one) => levelKey(one.label) === levelKey(band)) : -1;
550
+ if (level === -1) return null;
551
+ const probabilities = judged.levelProbabilities;
552
+ return {
553
+ level,
554
+ probabilities: Array.isArray(probabilities) ? levelDistribution(probabilities, levels.length, level) : null
555
+ };
556
+ }
557
+ /**
558
+ * A judgement on a score, as a fraction of its `maxScore`, read as
559
+ * `gradeFromRubric` reads one: a positive, finite maximum, and a ratio within
560
+ * 0 to 1, a hair outside it float noise and clamped. `null` when it has none.
561
+ */
562
+ function scoreOf$1(judged) {
563
+ const score = judged.score;
564
+ const max = judged.maxScore ?? 1;
565
+ if (typeof score !== "number" || !Number.isFinite(score) || !(Number.isFinite(max) && max > 0)) return null;
566
+ const ratio = score / max;
567
+ return ratio < -1e-9 || ratio > 1.000000001 ? null : Math.min(1, Math.max(0, ratio));
568
+ }
569
+ /**
570
+ * A grade's judgement of a criterion on its scale: its score from 0 to 1, and on
571
+ * levels the level's index (-1 on a score) and any distribution. `null` when it
572
+ * is not applicable or cannot be read.
573
+ */
574
+ function reading(grade, scale) {
575
+ const judged = criterionOf(grade, scale.name);
576
+ if (judged === void 0 || judged.notApplicable === true) return null;
577
+ if (scale.levels === null) {
578
+ const score = scoreOf$1(judged);
579
+ return score === null ? null : {
580
+ score,
581
+ level: -1,
582
+ probabilities: null
583
+ };
584
+ }
585
+ const read = levelOf(judged, scale.levels);
586
+ return read === null ? null : {
587
+ score: read.level / (scale.levels.length - 1),
588
+ ...read
589
+ };
590
+ }
591
+ function calibrateCriterion(scale, pairs) {
592
+ const model = [];
593
+ const person = [];
594
+ const modelLevels = [];
595
+ const personLevels = [];
596
+ const confidence = [];
597
+ const correct = [];
598
+ let brier = 0;
599
+ const human = {
600
+ first: [],
601
+ second: [],
602
+ firstLevels: [],
603
+ secondLevels: []
604
+ };
605
+ for (const pair of pairs) {
606
+ const m = reading(pair.model, scale);
607
+ const p = reading(pair.person, scale);
608
+ if (m === null || p === null) continue;
609
+ model.push(m.score);
610
+ person.push(p.score);
611
+ modelLevels.push(m.level);
612
+ personLevels.push(p.level);
613
+ if (m.probabilities !== null) {
614
+ confidence.push(m.probabilities[m.level]);
615
+ correct.push(m.level === p.level);
616
+ brier += m.probabilities.reduce((sum, probability, index) => sum + (probability - (index === p.level ? 1 : 0)) ** 2, 0);
617
+ }
618
+ const s = pair.secondPerson === void 0 ? null : reading(pair.secondPerson, scale);
619
+ if (s !== null) {
620
+ human.first.push(p.score);
621
+ human.second.push(s.score);
622
+ human.firstLevels.push(p.level);
623
+ human.secondLevels.push(s.level);
624
+ }
625
+ }
626
+ const n = model.length;
627
+ const shared = {
628
+ name: scale.name,
629
+ pairs: n,
630
+ pearson: pearson(model, person),
631
+ meanAbsoluteError: n === 0 ? null : mean(model.map((value, i) => Math.abs(value - person[i]))),
632
+ standardizedMeanDifference: standardizedMeanDifference(model, person)
633
+ };
634
+ const humanPairs = human.first.length;
635
+ const humanPearson = pearson(human.first, human.second);
636
+ if (scale.levels === null) return {
637
+ ...shared,
638
+ scale: "score",
639
+ ...humanPairs > 0 ? {
640
+ humanPairs,
641
+ humanPearson,
642
+ degradation: humanPearson === null || shared.pearson === null ? null : humanPearson - shared.pearson
643
+ } : {}
644
+ };
645
+ const k = scale.levels.length;
646
+ const kappa = quadraticKappa(personLevels, modelLevels, k);
647
+ const confusion = Array.from({ length: k }, () => new Array(k).fill(0));
648
+ personLevels.forEach((r, i) => {
649
+ confusion[r][modelLevels[i]] += 1;
650
+ });
651
+ const humanKappa = quadraticKappa(human.firstLevels, human.secondLevels, k);
652
+ return {
653
+ ...shared,
654
+ scale: "levels",
655
+ levels: k,
656
+ exactAgreement: n === 0 ? null : modelLevels.filter((level, i) => level === personLevels[i]).length / n,
657
+ adjacentAgreement: n === 0 ? null : modelLevels.filter((level, i) => Math.abs(level - personLevels[i]) <= 1).length / n,
658
+ quadraticKappa: kappa,
659
+ confusion,
660
+ probabilityPairs: confidence.length,
661
+ calibrationError: confidence.length === 0 ? null : expectedCalibrationError(confidence, correct),
662
+ brierScore: confidence.length === 0 ? null : brier / confidence.length,
663
+ ...humanPairs > 0 ? {
664
+ humanPairs,
665
+ humanQuadraticKappa: humanKappa,
666
+ humanPearson,
667
+ degradation: humanKappa === null || kappa === null ? null : humanKappa - kappa
668
+ } : {}
669
+ };
670
+ }
671
+ function calibrateTotal(pairs) {
672
+ const model = pairs.map((pair) => pair.model.score / pair.model.maxScore);
673
+ const person = pairs.map((pair) => pair.person.score / pair.person.maxScore);
674
+ const both = pairs.filter((pair) => pair.secondPerson !== void 0);
675
+ const humanPearson = pearson(both.map((pair) => pair.person.score / pair.person.maxScore), both.map((pair) => {
676
+ const second = pair.secondPerson;
677
+ return second.score / second.maxScore;
678
+ }));
679
+ const modelPearson = pearson(model, person);
680
+ return {
681
+ pairs: pairs.length,
682
+ pearson: modelPearson,
683
+ meanAbsoluteError: mean(model.map((value, i) => Math.abs(value - person[i]))),
684
+ standardizedMeanDifference: standardizedMeanDifference(model, person),
685
+ passAgreement: pairs.filter((pair) => pair.model.passed === pair.person.passed).length / pairs.length,
686
+ ...both.length > 0 ? {
687
+ humanPairs: both.length,
688
+ humanPearson,
689
+ degradation: humanPearson === null || modelPearson === null ? null : humanPearson - modelPearson
690
+ } : {}
691
+ };
692
+ }
693
+ /**
694
+ * How one grader configuration — a model, a prompt revision, a rubric and a
695
+ * target language — agrees with people, measured on responses the host had
696
+ * marked twice. Pure: the same pairs give the same report and the same
697
+ * `reportHash`.
698
+ *
699
+ * Every model grade must come from one configuration (its `grader` names the
700
+ * model and the prompt), and no person's grade may be a model's. A criterion
701
+ * is measured over the pairs where both judged it on its scale — levels by
702
+ * their labels, scores as fractions of their maximum — and every metric that
703
+ * is undefined is `null`, never a number made up. It ships no threshold:
704
+ * `routeGrade` applies the host's.
705
+ */
706
+ function calibrationReport(pairs, scope) {
707
+ if (pairs.length === 0) return {
708
+ ok: false,
709
+ reason: "No pairs: a calibration needs responses marked by the model and by a person."
710
+ };
711
+ const read = rubricScales(scope.rubric);
712
+ if (!read.ok) return {
713
+ ok: false,
714
+ reason: `Criterion "${read.name}" has levels the grading functions cannot read.`
715
+ };
716
+ const scales = read.scales;
717
+ if (scales.length === 0) return {
718
+ ok: false,
719
+ reason: "No rubric: a calibration measures a grader criterion by criterion."
720
+ };
721
+ let configuration = null;
722
+ for (const [index, pair] of pairs.entries()) {
723
+ if ([
724
+ pair.model,
725
+ pair.person,
726
+ ...pair.secondPerson ? [pair.secondPerson] : []
727
+ ].some((grade) => !isGradeInRange(grade.score, grade.maxScore))) return {
728
+ ok: false,
729
+ reason: `Pair ${index + 1} has a grade whose numbers cannot be a grade.`
730
+ };
731
+ if ([pair.person, pair.secondPerson].some((grade) => grade?.grader !== void 0 && grade.grader.kind !== "human")) return {
732
+ ok: false,
733
+ reason: `Pair ${index + 1} has a person's grade that a grader other than a person gave.`
734
+ };
735
+ const own = graderConfiguration(pair.model, scope);
736
+ if (own === null) return {
737
+ ok: false,
738
+ reason: `Pair ${index + 1}'s model grade does not name its model and prompt (grader.model, grader.promptHash), so it cannot be calibrated.`
739
+ };
740
+ if (configuration !== null && !sameConfiguration(configuration, own)) return {
741
+ ok: false,
742
+ reason: `Pair ${index + 1}'s model grade comes from another configuration: a report measures one model, prompt, rubric and target.`
743
+ };
744
+ configuration = own;
745
+ }
746
+ const report = {
747
+ configuration,
748
+ pairs: pairs.length,
749
+ criteria: scales.map((scale) => calibrateCriterion(scale, pairs)),
750
+ total: calibrateTotal(pairs)
751
+ };
752
+ return {
753
+ ok: true,
754
+ report: {
755
+ ...report,
756
+ reportHash: contentHash(report)
757
+ }
758
+ };
759
+ }
760
+ //#endregion
361
761
  //#region src/grading.ts
362
762
  /**
363
763
  * Computes the weighted total from per-criterion scores — the arithmetic that
@@ -485,6 +885,260 @@ function hasGrade(outcome) {
485
885
  return outcome.status === "scored" || outcome.status === "graded";
486
886
  }
487
887
  //#endregion
888
+ //#region src/scoring/assisted/grade-from-judgement.ts
889
+ /**
890
+ * The grade a checked model judgement gives a written response: the total
891
+ * computed as `gradeFromRubric` computes every grade — never by the model — with
892
+ * the item's own weights and pass line, and the record a host keeps.
893
+ *
894
+ * Each criterion scores what `checkAiGrading` read: a level's place on its
895
+ * scale, or the model's score from 0 to 1. The level chosen is kept as `band`,
896
+ * its probabilities as `levelProbabilities`, and the model's identity as
897
+ * `grader` (`kind: 'ai'`, with its `model` and `promptHash`), so a routing rule
898
+ * and a calibration can read them later.
899
+ *
900
+ * `data` must be the item the request was built from: a judgement that names a
901
+ * criterion this rubric does not have, or leaves one of its criteria out, is
902
+ * `unscorable` rather than a grade on another task's rubric.
903
+ */
904
+ function gradeFromJudgement(judgement, data, options = {}) {
905
+ const authored = data.rubric?.criteria ?? [];
906
+ const byName = new Map(authored.map((criterion) => [levelKey(criterion.name), criterion]));
907
+ if (byName.size !== authored.length) return {
908
+ unscorable: true,
909
+ reason: "This item's rubric names a criterion twice, so a judgement cannot say which of the two it grades."
910
+ };
911
+ const judged = /* @__PURE__ */ new Set();
912
+ const criteria = [];
913
+ for (const one of judgement.criteria) {
914
+ const criterion = byName.get(levelKey(one.name));
915
+ if (criterion === void 0 || judged.has(levelKey(one.name))) return {
916
+ unscorable: true,
917
+ reason: `The judgement names criterion "${one.name}", which this item's rubric does not have once. Grade it with the item it was asked about.`
918
+ };
919
+ judged.add(levelKey(one.name));
920
+ criteria.push({
921
+ name: criterion.name,
922
+ score: one.score,
923
+ weight: criterion.weight,
924
+ ...one.level !== void 0 ? { band: one.level } : {},
925
+ ...one.probabilities !== void 0 ? { levelProbabilities: [...one.probabilities] } : {},
926
+ ...one.comment !== void 0 ? { comment: one.comment } : {}
927
+ });
928
+ }
929
+ const unjudged = authored.find((criterion) => !judged.has(levelKey(criterion.name)));
930
+ if (unjudged !== void 0) return {
931
+ unscorable: true,
932
+ reason: `The judgement leaves out criterion "${unjudged.name}". A total over fewer criteria would grade a different task.`
933
+ };
934
+ const grade = gradeFromRubric(criteria, data, {
935
+ ...options,
936
+ feedback: judgement.feedback
937
+ });
938
+ if ("unscorable" in grade) return grade;
939
+ const { provenance, usage } = judgement;
940
+ return {
941
+ ...grade,
942
+ ...judgement.corrections.length > 0 ? { corrections: [...judgement.corrections] } : {},
943
+ ...judgement.rationale !== void 0 ? { rationale: judgement.rationale } : {},
944
+ requiresHumanReview: judgement.requiresHumanReview,
945
+ grader: {
946
+ kind: "ai",
947
+ ...provenance?.model !== void 0 ? { model: provenance.model } : {},
948
+ ...provenance?.promptHash !== void 0 ? { promptHash: provenance.promptHash } : {}
949
+ },
950
+ ...usage !== void 0 ? { usage } : {},
951
+ ...provenance?.generatedAt !== void 0 ? { gradedAt: provenance.generatedAt } : {}
952
+ };
953
+ }
954
+ //#endregion
955
+ //#region src/shuffle.ts
956
+ /**
957
+ * Seeded, deterministic shuffling — the ONE algorithm every presentation-order
958
+ * decision in the SDK goes through: option order in a multiple-choice item,
959
+ * item order inside an item group, entry order in a sequence. A server and a
960
+ * client holding the same seed derive the same order, and an order recorded
961
+ * against an attempt can be rebuilt later from nothing but its seed.
962
+ *
963
+ * STABILITY: the hash and the generator are part of the wire contract. A
964
+ * stored attempt may hold only a seed and rely on this function to reproduce
965
+ * the order it presented; changing either would silently re-order every
966
+ * recorded attempt. Any change here is a package major, and the pinned
967
+ * permutation test exists so an accidental one fails loudly.
968
+ */
969
+ /** FNV-1a hash of a string → unsigned 32-bit integer. */
970
+ function hashSeed(input) {
971
+ let h = 2166136261;
972
+ for (let i = 0; i < input.length; i += 1) {
973
+ h ^= input.charCodeAt(i);
974
+ h = Math.imul(h, 16777619) >>> 0;
975
+ }
976
+ return h;
977
+ }
978
+ /** LCG-driven Fisher–Yates over a 32-bit seed; returns a permutation (no loss, no duplicate). */
979
+ function shuffleWithSeed(items, seed, version) {
980
+ const out = [...items];
981
+ let s = seed >>> 0 || 1;
982
+ for (let i = out.length - 1; i > 0; i -= 1) {
983
+ s = Math.imul(s, 1664525) + 1013904223 >>> 0;
984
+ const j = version === 2 ? Math.floor(s / 4294967296 * (i + 1)) : s % (i + 1);
985
+ [out[i], out[j]] = [out[j], out[i]];
986
+ }
987
+ return out;
988
+ }
989
+ function seededShuffle(items, seed, options = {}) {
990
+ return shuffleWithSeed(items, hashSeed(seed), options.version ?? 1);
991
+ }
992
+ //#endregion
993
+ //#region src/scoring/assisted/routing.ts
994
+ /** A threshold the caller configured: a finite number, and a rate within 0 to 1 where it is one. */
995
+ function checkThreshold(name, value, rate) {
996
+ if (value === void 0) return;
997
+ if (typeof value !== "number" || !Number.isFinite(value) || rate && (value < 0 || value > 1)) throw new RangeError(`routeGrade: policy ${name} must be a finite number${rate ? " from 0 to 1" : ""}; got ${String(value)}.`);
998
+ }
999
+ function checkPolicy(policy) {
1000
+ const thresholds = policy.calibration ?? {};
1001
+ checkThreshold("calibration.minPairs", thresholds.minPairs, false);
1002
+ checkThreshold("calibration.minQuadraticKappa", thresholds.minQuadraticKappa, false);
1003
+ checkThreshold("calibration.minExactAgreement", thresholds.minExactAgreement, true);
1004
+ checkThreshold("calibration.minPearson", thresholds.minPearson, false);
1005
+ checkThreshold("calibration.maxMeanAbsoluteError", thresholds.maxMeanAbsoluteError, true);
1006
+ checkThreshold("calibration.maxAbsStandardizedMeanDifference", thresholds.maxAbsStandardizedMeanDifference, false);
1007
+ checkThreshold("calibration.maxDegradation", thresholds.maxDegradation, false);
1008
+ checkThreshold("calibration.maxCalibrationError", thresholds.maxCalibrationError, true);
1009
+ checkThreshold("minLevelProbability", policy.minLevelProbability, true);
1010
+ if (policy.audit !== void 0) {
1011
+ checkThreshold("audit.rate", policy.audit.rate, true);
1012
+ if (typeof policy.audit.seed !== "string") throw new RangeError("routeGrade: policy audit.seed must be a string.");
1013
+ }
1014
+ }
1015
+ function checkContext(context, policy) {
1016
+ if (context.mode !== "exam" && context.mode !== "practice") throw new RangeError(`routeGrade: mode must be 'exam' or 'practice'; got ${String(context.mode)}.`);
1017
+ if (policy.audit !== void 0 && typeof context.sampleKey !== "string") throw new RangeError("routeGrade: a policy with an audit needs a sampleKey, the response's stable identity.");
1018
+ }
1019
+ /** At least `min`, when there is a minimum; a missing value fails it. */
1020
+ const atLeast = (value, min) => min === void 0 || typeof value === "number" && value >= min;
1021
+ /** At most `max`, when there is a maximum; a missing value fails it. */
1022
+ const atMost = (value, max) => max === void 0 || typeof value === "number" && value <= max;
1023
+ const absolute = (value) => value === null ? null : Math.abs(value);
1024
+ function criterionMeets(criterion, t) {
1025
+ const shared = atLeast(criterion.pairs, t.minPairs) && atMost(criterion.meanAbsoluteError, t.maxMeanAbsoluteError) && atMost(absolute(criterion.standardizedMeanDifference), t.maxAbsStandardizedMeanDifference) && atMost(criterion.degradation, t.maxDegradation);
1026
+ if (criterion.scale === "score") return shared && atLeast(criterion.pearson, t.minPearson);
1027
+ return shared && atLeast(criterion.quadraticKappa, t.minQuadraticKappa) && atLeast(criterion.exactAgreement, t.minExactAgreement) && (criterion.probabilityPairs === 0 || atMost(criterion.calibrationError, t.maxCalibrationError));
1028
+ }
1029
+ /** Whether a report meets every threshold, on every criterion and on the total. */
1030
+ function reportMeets(report, t) {
1031
+ const total = report.total;
1032
+ return report.criteria.every((criterion) => criterionMeets(criterion, t)) && atLeast(total.pairs, t.minPairs) && atLeast(total.pearson, t.minPearson) && atMost(total.meanAbsoluteError, t.maxMeanAbsoluteError) && atMost(absolute(total.standardizedMeanDifference), t.maxAbsStandardizedMeanDifference) && atMost(total.degradation, t.maxDegradation);
1033
+ }
1034
+ /**
1035
+ * Whether a criterion judged on levels is less sure than `minimum`: the
1036
+ * grader gave its chosen level a lower probability, or the grade has no
1037
+ * distribution over the levels to read, or no level at all. A rubric whose
1038
+ * levels cannot be read is unsure throughout.
1039
+ */
1040
+ function isUnsure(grade, item, minimum) {
1041
+ const read = rubricScales(item.rubric);
1042
+ if (!read.ok) return true;
1043
+ return read.scales.some((scale) => {
1044
+ const judged = criterionOf(grade, scale.name);
1045
+ if (scale.levels === null || judged?.notApplicable === true) return false;
1046
+ const level = judged === void 0 ? null : levelOf(judged, scale.levels);
1047
+ return level === null || level.probabilities === null || level.probabilities[level.level] < minimum;
1048
+ });
1049
+ }
1050
+ /**
1051
+ * Murmur3's 32-bit finaliser, so every bit of the hash moves every bit of the
1052
+ * result. FNV-1a alone barely moves its high bits for a change in a key's last
1053
+ * characters, and the sample reads the high bits: `attempt-1:slot-1` and
1054
+ * `attempt-1:slot-2` were drawn together 97 times in 100 at a rate of 0.25.
1055
+ */
1056
+ function avalanche(hash) {
1057
+ let mixed = hash ^ hash >>> 16;
1058
+ mixed = Math.imul(mixed, 2246822507);
1059
+ mixed ^= mixed >>> 13;
1060
+ mixed = Math.imul(mixed, 3266489909);
1061
+ mixed ^= mixed >>> 16;
1062
+ return mixed >>> 0;
1063
+ }
1064
+ /**
1065
+ * Whether `key` is in the audit sample: a stable share `rate` of keys, chosen
1066
+ * by a hash of the seed and the key, so the same response is always in or
1067
+ * always out, two responses are drawn independently even when their keys
1068
+ * differ by one character, and changing the seed draws a new sample.
1069
+ */
1070
+ function inAuditSample(key, rate, seed) {
1071
+ checkThreshold("audit.rate", rate, true);
1072
+ if (typeof key !== "string" || typeof seed !== "string") throw new RangeError("inAuditSample: the key and the seed must be strings.");
1073
+ return avalanche(hashSeed(`${seed}\u0000${key}`)) / 2 ** 32 < rate;
1074
+ }
1075
+ /**
1076
+ * Where a model's grade goes: to count, or to a person first. Deterministic —
1077
+ * the same grade, context and policy always route the same way — and the
1078
+ * decision says every reason it rests on, in {@link GradeRoutingReason}'s
1079
+ * order, so an appeal can see why a person did or did not mark a response.
1080
+ *
1081
+ * - **In `exam`, a grade needs a calibration of its exact configuration**
1082
+ * (`no-calibration`): the model and prompt its `grader` names, and the
1083
+ * item's rubric and target. Fail closed: without a matching report, every
1084
+ * grade goes to a person.
1085
+ * - A supplied report must meet the policy's thresholds (`calibration-below-policy`).
1086
+ * - A criterion on levels the grader was less sure of than
1087
+ * `minLevelProbability`, or showed no probabilities for (`unsure`), a
1088
+ * grader that asked for review (`model-asked`), a `low` confidence where
1089
+ * the policy routes on it (`low-confidence`), and the audit sample
1090
+ * (`audit-sample`) send it to a person too.
1091
+ *
1092
+ * A mode other than `exam` or `practice`, a policy threshold that is not a
1093
+ * finite number, a rate outside 0 to 1, or an audit without a `sampleKey`
1094
+ * throws a `RangeError`: it is the caller's configuration, not the grade's,
1095
+ * and a typo must not quietly make an exam a practice.
1096
+ */
1097
+ function routeGrade(grade, context) {
1098
+ const policy = context.policy ?? {};
1099
+ checkPolicy(policy);
1100
+ checkContext(context, policy);
1101
+ const reasons = [];
1102
+ const own = graderConfiguration(grade, context.item);
1103
+ const report = context.calibration;
1104
+ const matching = report !== void 0 && own !== null && sameConfiguration(own, report.configuration) ? report : void 0;
1105
+ if (context.mode === "exam" && matching === void 0) reasons.push("no-calibration");
1106
+ if (matching !== void 0 && !reportMeets(matching, policy.calibration ?? {})) reasons.push("calibration-below-policy");
1107
+ const minimum = policy.minLevelProbability;
1108
+ if (minimum !== void 0 && isUnsure(grade, context.item, minimum)) reasons.push("unsure");
1109
+ if (grade.requiresHumanReview === true) reasons.push("model-asked");
1110
+ if (policy.routeLowConfidence === true && grade.confidence === "low") reasons.push("low-confidence");
1111
+ if (policy.audit !== void 0 && inAuditSample(context.sampleKey, policy.audit.rate, policy.audit.seed)) reasons.push("audit-sample");
1112
+ return {
1113
+ to: reasons.length === 0 ? "model" : "person",
1114
+ reasons,
1115
+ ...matching !== void 0 ? { calibration: {
1116
+ reportHash: matching.reportHash,
1117
+ pairs: matching.pairs
1118
+ } } : {}
1119
+ };
1120
+ }
1121
+ /**
1122
+ * The outcome a routed grade gives its slot. To the model: the grade, with the
1123
+ * decision on its `routing`, lifted as `outcomeFromGrade` lifts any grade. To a
1124
+ * person: `{ status: 'deferred', reason: 'awaiting_human_review', pendingGrade }`
1125
+ * — not `graded`, so its `passed` is never read as a verdict, and a composed
1126
+ * attempt stays `provisional` until a person's grade replaces it.
1127
+ */
1128
+ function outcomeFromRoutedGrade(grade, routing) {
1129
+ const routed = {
1130
+ ...grade,
1131
+ routing
1132
+ };
1133
+ if (routing.to === "model") return outcomeFromGrade(routed);
1134
+ return {
1135
+ status: "deferred",
1136
+ reason: "awaiting_human_review",
1137
+ maxScore: 1,
1138
+ pendingGrade: routed
1139
+ };
1140
+ }
1141
+ //#endregion
488
1142
  //#region src/scoring/compose.ts
489
1143
  function earned(item) {
490
1144
  const { outcome } = item;
@@ -1599,6 +2253,6 @@ function costed(data, opening, graded, rules, rounding) {
1599
2253
  };
1600
2254
  }
1601
2255
  //#endregion
1602
- export { validateItemScoringPolicy as C, scoreTries as S, gte as _, inspectWav as a, ITEM_SCORING_MAX_RETRIES as b, validateSpeechAssessment as c, gradeFromRubric as d, hasGrade as f, classifyBand as g, computePassThreshold as h, outcomeFromUnscorable as i, diffDictationChars as l, DEFAULT_PASS_THRESHOLD as m, evaluateTries as n, gradeReadAloud as o, outcomeFromGrade as p, score as r, alignReadAloud as s, evaluate as t, composeAssessmentScore as u, roundGrade as v, isGradeInRange as w, resolveItemScoringPolicy as x, DEFAULT_ITEM_SCORING_POLICY as y };
2256
+ export { DEFAULT_ITEM_SCORING_POLICY as A, contentHash as C, classifyBand as D, computePassThreshold as E, isGradeInRange as F, resolveItemScoringPolicy as M, scoreTries as N, gte as O, validateItemScoringPolicy as P, canonicalJson as S, DEFAULT_PASS_THRESHOLD as T, gradeFromRubric as _, inspectWav as a, calibrationReport as b, validateSpeechAssessment as c, inAuditSample as d, outcomeFromRoutedGrade as f, gradeFromJudgement as g, seededShuffle as h, outcomeFromUnscorable as i, ITEM_SCORING_MAX_RETRIES as j, roundGrade as k, diffDictationChars as l, hashSeed as m, evaluateTries as n, gradeReadAloud as o, routeGrade as p, score as r, alignReadAloud as s, evaluate as t, composeAssessmentScore as u, hasGrade as v, fingerprint as w, graderConfiguration as x, outcomeFromGrade as y };
1603
2257
 
1604
- //# sourceMappingURL=scoring-BZqUkuDF.js.map
2258
+ //# sourceMappingURL=scoring-BKNNTz-s.js.map