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