@intellectif/lk-core 1.4.0 → 1.6.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.
- package/CHANGELOG.md +102 -0
- package/README.md +2 -0
- package/dist/ai-check.cjs +175 -17
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +13 -5
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +13 -5
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +175 -17
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-CnmLPMzd.d.ts → ai-i3jwXnpX.d.cts} +242 -7
- package/dist/ai-i3jwXnpX.d.cts.map +1 -0
- package/dist/{ai-CnmLPMzd.d.cts → ai-i3jwXnpX.d.ts} +242 -7
- package/dist/ai-i3jwXnpX.d.ts.map +1 -0
- package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-C4YRVrFi.cjs} +683 -72
- package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
- package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-OJ5m_1EB.js} +663 -88
- package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
- package/dist/{index-BqMXWuNC.d.cts → index--U7hHV0s.d.cts} +2 -2
- package/dist/{index-BqMXWuNC.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
- package/dist/{index-B54baFUA.d.ts → index-CFRgZLhH.d.cts} +45 -12
- package/dist/index-CFRgZLhH.d.cts.map +1 -0
- package/dist/{index-CVWvymVb.d.ts → index-DeD8EI1D.d.ts} +327 -3
- package/dist/index-DeD8EI1D.d.ts.map +1 -0
- package/dist/{index-CcHDTxIZ.d.ts → index-Dj92T76o.d.ts} +2 -2
- package/dist/{index-CcHDTxIZ.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
- package/dist/{index-BAfEHCnD.d.cts → index-Uh1u57yq.d.cts} +327 -3
- package/dist/index-Uh1u57yq.d.cts.map +1 -0
- package/dist/{index-2nigSf4N.d.cts → index-WBHSrIUU.d.ts} +45 -12
- package/dist/index-WBHSrIUU.d.ts.map +1 -0
- package/dist/index.cjs +88 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -95
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +217 -95
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -118
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-BbvvH_Ob.js} +3 -2
- package/dist/item-group-BbvvH_Ob.js.map +1 -0
- package/dist/{item-group-DroFXgLb.cjs → item-group-CQRbb8Rx.cjs} +3 -2
- package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-B23o9UWh.js} +218 -21
- package/dist/registry-B23o9UWh.js.map +1 -0
- package/dist/{registry-DVi0LR_i.cjs → registry-Ih5n4PVZ.cjs} +265 -20
- package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
- package/dist/{schemas-DVbAOrtd.cjs → schemas-Q2dyOgc2.cjs} +77 -14
- package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-yId-bJhD.js} +72 -15
- package/dist/schemas-yId-bJhD.js.map +1 -0
- package/dist/schemas.cjs +5 -2
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +4 -3
- package/dist/{scoring-BZqUkuDF.js → scoring-CLMdk8i9.js} +657 -3
- package/dist/scoring-CLMdk8i9.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-CaPS6q8b.cjs} +722 -2
- package/dist/scoring-CaPS6q8b.cjs.map +1 -0
- package/dist/scoring.cjs +8 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-DNzkSJ0S.cjs → xapi-BX0hvRRB.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-Cdk9fTvV.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-Cdk9fTvV.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -2
- package/package.json +2 -2
- package/vectors/README.md +24 -1
- package/vectors/scoring.json +18182 -1
- package/dist/ai-CnmLPMzd.d.cts.map +0 -1
- package/dist/ai-CnmLPMzd.d.ts.map +0 -1
- package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
- package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
- package/dist/index-2nigSf4N.d.cts.map +0 -1
- package/dist/index-B54baFUA.d.ts.map +0 -1
- package/dist/index-BAfEHCnD.d.cts.map +0 -1
- package/dist/index-CVWvymVb.d.ts.map +0 -1
- package/dist/item-group-DXiR8nOr.js.map +0 -1
- package/dist/item-group-DroFXgLb.cjs.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.map +0 -1
- package/dist/schemas-Bdw0nGPo.js.map +0 -1
- package/dist/schemas-DVbAOrtd.cjs.map +0 -1
- package/dist/scoring-B2_HEhMT.cjs.map +0 -1
- package/dist/scoring-BZqUkuDF.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as DeferredScoringError, Dt as maxUnits, F as levelKey, I as readRubricLevels, L as countWords, P as levelDistribution, Q as ActivitySchemaError, T as SPEECH_ASSESSMENT_MAX_WORDS, at as dictationReferenceWords, dt as codePointLength, et as RedactedScoringError, ft as normalizeDictationText, it as alignSequences, nt as STEP, t as getActivityTypeDescriptor, tt as UnknownActivityTypeError, v as ReadAloudDataSchema, w as SPEECH_ASSESSMENT_MAX_TEXT_LENGTH, y as CANONICAL_LOCALE_RE } from "./registry-B23o9UWh.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 {
|
|
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-
|
|
2258
|
+
//# sourceMappingURL=scoring-CLMdk8i9.js.map
|