@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.
- package/CHANGELOG.md +59 -0
- package/README.md +2 -0
- package/dist/{ai-B222dtHH.d.ts → ai-BYryFkF9.d.cts} +462 -5
- package/dist/ai-BYryFkF9.d.cts.map +1 -0
- package/dist/{ai-B222dtHH.d.cts → ai-BYryFkF9.d.ts} +462 -5
- package/dist/ai-BYryFkF9.d.ts.map +1 -0
- package/dist/ai-check.cjs +274 -23
- 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 +274 -23
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-Cen7-3qs.cjs} +520 -58
- package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
- package/dist/{ai-writing-CCBjTOM3.js → ai-writing-D8PdhJJz.js} +513 -75
- package/dist/ai-writing-D8PdhJJz.js.map +1 -0
- package/dist/{index-Dyu66R4-.d.cts → index-Bg0aeWtM.d.cts} +327 -3
- package/dist/index-Bg0aeWtM.d.cts.map +1 -0
- package/dist/{index-BWPRPIXL.d.ts → index-CxpiWedW.d.ts} +7 -217
- package/dist/index-CxpiWedW.d.ts.map +1 -0
- package/dist/{index-CXyOV8sm.d.cts → index-D7R3Ll8R.d.cts} +2 -2
- package/dist/{index-CXyOV8sm.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
- package/dist/{index-UGxRSopE.d.ts → index-DUaW1Tj5.d.ts} +327 -3
- package/dist/index-DUaW1Tj5.d.ts.map +1 -0
- package/dist/{index-O4_ExlAD.d.cts → index-qMbxed1B.d.cts} +7 -217
- package/dist/index-qMbxed1B.d.cts.map +1 -0
- package/dist/{index-CegM8o15.d.ts → index-wMc1E_aM.d.ts} +2 -2
- package/dist/{index-CegM8o15.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
- package/dist/index.cjs +32 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -97
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +115 -97
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -117
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
- package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
- package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
- package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
- package/dist/registry-Ce00A_OW.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
- package/dist/registry-DaZRpqjW.js.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
- package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
- package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
- package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
- package/dist/schemas.cjs +2 -2
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +2 -2
- package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
- package/dist/scoring-BKNNTz-s.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
- package/dist/scoring-lKQVYsxy.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-NYrTlpID.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.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-B222dtHH.d.cts.map +0 -1
- package/dist/ai-B222dtHH.d.ts.map +0 -1
- package/dist/ai-writing-CCBjTOM3.js.map +0 -1
- package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
- package/dist/index-BWPRPIXL.d.ts.map +0 -1
- package/dist/index-Dyu66R4-.d.cts.map +0 -1
- package/dist/index-O4_ExlAD.d.cts.map +0 -1
- package/dist/index-UGxRSopE.d.ts.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.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
|
-
const require_registry = require("./registry-
|
|
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-
|
|
2468
|
+
//# sourceMappingURL=scoring-lKQVYsxy.cjs.map
|