@intellectif/lk-core 1.10.0 → 1.10.1

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.
@@ -222,16 +222,27 @@ const ROUNDING_MODES = [
222
222
  const MAX_ROUNDING_DP = 15;
223
223
  /**
224
224
  * The rounding policy `value` holds, or `undefined` for none (`undefined` or
225
- * `null`). A malformed one would reach {@link gte} as `10 ** undefined` and
226
- * turn a perfect score into a fail with no error, so it throws here instead.
225
+ * `null`): the check for an optional `rounding`. Anything else is checked by
226
+ * {@link requireRoundingPolicy}.
227
227
  *
228
- * Internal. Every function that takes a `rounding` option checks it here, so
229
- * each refuses the same policies with the same message; `scoring/index.ts`
230
- * does not re-export it.
228
+ * Internal, as is {@link requireRoundingPolicy}: `scoring/index.ts` re-exports
229
+ * neither.
231
230
  */
232
231
  function roundingPolicyOf(value) {
233
- if (value === void 0 || value === null) return;
234
- const { mode, dp } = value;
232
+ return value === void 0 || value === null ? void 0 : requireRoundingPolicy(value);
233
+ }
234
+ /**
235
+ * The rounding policy `value` holds. A malformed one would reach the rounding
236
+ * as `10 ** undefined` and turn a perfect score into a fail with no error, so
237
+ * it throws here instead; so does a missing one, `undefined` or `null`, where a
238
+ * policy is required.
239
+ *
240
+ * Every function that takes a policy, as an argument or a `rounding` option,
241
+ * checks it here or through {@link roundingPolicyOf}, so each refuses the same
242
+ * policies with the same message.
243
+ */
244
+ function requireRoundingPolicy(value) {
245
+ const { mode, dp } = value ?? {};
235
246
  if (!ROUNDING_MODES.includes(mode) || typeof dp !== "number" || !Number.isInteger(dp) || dp < 0 || dp > MAX_ROUNDING_DP) {
236
247
  const describe = (field) => typeof field === "string" ? JSON.stringify(field) : typeof field === "number" || field === void 0 ? String(field) : typeof field === "object" ? "an object" : `a ${typeof field}`;
237
248
  throw new RangeError(`Invalid rounding policy (mode ${describe(mode)}, dp ${describe(dp)}): expected { mode: 'half-up' | 'half-even' | 'floor' | 'ceil', dp: a whole number from 0 to ${MAX_ROUNDING_DP} }.`);
@@ -262,8 +273,9 @@ function scaled(value, dp) {
262
273
  * Normalises `-0` to `0` without touching anything else. `-0` JSON-serialises
263
274
  * as `0` but fails `Object.is(-0, 0)`, which surprises a strict-equality
264
275
  * gradebook comparison. Deliberately NOT `|| 0`: that also swallowed `NaN`
265
- * (reachable with an absurd `dp`, where `10 ** dp` overflows to Infinity) and
266
- * turned a visible failure into a plausible-looking grade of zero.
276
+ * (reached with an absurd `dp`, where `10 ** dp` overflows to Infinity, before
277
+ * policies were checked) and turned a visible failure into a plausible-looking
278
+ * grade of zero.
267
279
  */
268
280
  function noNegZero(value) {
269
281
  return Object.is(value, -0) ? 0 : value;
@@ -274,8 +286,15 @@ function noNegZero(value) {
274
286
  * Idempotent for every mode: rounding an already-rounded value returns it
275
287
  * unchanged. That matters because a composed score is rounded once and then
276
288
  * compared through {@link gte}, which rounds again.
289
+ *
290
+ * @throws RangeError when `policy` is not a {@link RoundingPolicy} — for a
291
+ * non-finite `value` too, which a policy returns unchanged.
277
292
  */
278
293
  function roundGrade(value, policy) {
294
+ return round(value, requireRoundingPolicy(policy));
295
+ }
296
+ /** {@link roundGrade} under a policy {@link requireRoundingPolicy} returned. */
297
+ function round(value, policy) {
279
298
  if (!Number.isFinite(value)) return value;
280
299
  const factor = 10 ** policy.dp;
281
300
  const raw = scaled(value, policy.dp);
@@ -306,9 +325,13 @@ function halfAway(raw) {
306
325
  * shown one number and recorded against another. Rounding both sides — and
307
326
  * allowing an epsilon — makes "what the learner sees" and "what the gradebook
308
327
  * decides" the same comparison.
328
+ *
329
+ * @throws RangeError when `policy` is not a {@link RoundingPolicy}. It is read
330
+ * once, so both sides are rounded under the same one.
309
331
  */
310
332
  function gte(value, threshold, policy) {
311
- return roundGrade(value, policy) >= roundGrade(threshold, policy) - EPSILON;
333
+ const checked = requireRoundingPolicy(policy);
334
+ return round(value, checked) >= round(threshold, checked) - EPSILON;
312
335
  }
313
336
  /**
314
337
  * Classifies a value into a band by **flooring**: the highest band whose `min`
@@ -337,10 +360,16 @@ const DEFAULT_PASS_THRESHOLD = .7;
337
360
  * the rounding band, and this SDK does not alter historical grades without an
338
361
  * explicit decision. Absent, the comparison is the exact raw `>=` it has
339
362
  * always been.
363
+ *
364
+ * The policy is checked at the call, as `score()`'s `rounding` option is:
365
+ * `null` reads as no policy, and one that cannot be applied — an unknown
366
+ * `mode`, or a `dp` that is not a whole number from 0 to 15 — throws a
367
+ * `RangeError` instead of quietly failing the comparison.
340
368
  */
341
369
  function computePassThreshold(activityData, score, rounding) {
370
+ const policy = roundingPolicyOf(rounding);
342
371
  const threshold = activityData.passThreshold ?? .7;
343
- return rounding === void 0 ? score >= threshold : gte(score, threshold, rounding);
372
+ return policy === void 0 ? score >= threshold : gte(score, threshold, policy);
344
373
  }
345
374
  /**
346
375
  * Selects the authored overall feedback for a result: `feedback.correct` when
@@ -1161,8 +1190,11 @@ function rejectedField(slotIds) {
1161
1190
  * until every item has a grade. An outcome whose score cannot be a grade out of
1162
1191
  * its `maxScore` is not a grade either: it is held the same way and named in
1163
1192
  * `rejectedSlotIds`.
1193
+ *
1194
+ * @throws RangeError when `policy.rounding` is not a `RoundingPolicy`.
1164
1195
  */
1165
1196
  function composeAssessmentScore(sections, policy) {
1197
+ const rounding = requireRoundingPolicy(policy.rounding);
1166
1198
  const pendingAll = [];
1167
1199
  const rejectedAll = [];
1168
1200
  const unscorableAll = [];
@@ -1200,7 +1232,7 @@ function composeAssessmentScore(sections, policy) {
1200
1232
  earnedPoints,
1201
1233
  gradedMaxPoints,
1202
1234
  maxPoints,
1203
- score: roundGrade(raw, policy.rounding),
1235
+ score: roundGrade(raw, rounding),
1204
1236
  appliedThreshold,
1205
1237
  pendingSlotIds,
1206
1238
  rejectedSlotIds,
@@ -1217,13 +1249,13 @@ function composeAssessmentScore(sections, policy) {
1217
1249
  gradedMaxPoints: partial.gradedMaxPoints,
1218
1250
  maxPoints: partial.maxPoints,
1219
1251
  score: partial.score,
1220
- passed: partial.appliedThreshold === null || partial.gradedMaxPoints === 0 ? true : gte(partial.score, partial.appliedThreshold, policy.rounding),
1252
+ passed: partial.appliedThreshold === null || partial.gradedMaxPoints === 0 ? true : gte(partial.score, partial.appliedThreshold, rounding),
1221
1253
  appliedThreshold: partial.appliedThreshold,
1222
1254
  pendingSlotIds: partial.pendingSlotIds,
1223
1255
  ...rejectedField(partial.rejectedSlotIds),
1224
1256
  unscorableSlotIds: partial.unscorableSlotIds
1225
1257
  }));
1226
- const score = roundGrade(scored.reduce((sum, section) => sum + section.score * section.normalizedWeight, 0), policy.rounding);
1258
+ const score = roundGrade(scored.reduce((sum, section) => sum + section.score * section.normalizedWeight, 0), rounding);
1227
1259
  const status = pendingAll.length > 0 ? "provisional" : "final";
1228
1260
  if (status === "provisional") return {
1229
1261
  sections: scored,
@@ -1245,7 +1277,7 @@ function composeAssessmentScore(sections, policy) {
1245
1277
  ...rejectedField(rejectedAll),
1246
1278
  unscorableSlotIds: unscorableAll
1247
1279
  };
1248
- const overallOk = gte(score, policy.passThreshold, policy.rounding);
1280
+ const overallOk = gte(score, policy.passThreshold, rounding);
1249
1281
  const sectionsOk = scored.every((section) => section.passed);
1250
1282
  const passed = overallOk && sectionsOk;
1251
1283
  return {
@@ -2423,4 +2455,4 @@ function costed(data, opening, graded, rules, rounding) {
2423
2455
  }
2424
2456
  export { roundGrade as A, canonicalJson as C, computePassThreshold as D, DEFAULT_PASS_THRESHOLD as E, validateItemScoringPolicy as F, isGradeInRange as I, ITEM_SCORING_MAX_RETRIES as M, resolveItemScoringPolicy as N, classifyBand as O, scoreTries as P, graderConfiguration as S, fingerprint as T, gradeFromJudgement as _, inspectWav as a, outcomeFromGrade as b, alignReadAloud as c, composeAssessmentScore as d, inAuditSample as f, seededShuffle as g, hashSeed as h, outcomeFromUnscorable as i, DEFAULT_ITEM_SCORING_POLICY as j, gte as k, validateSpeechAssessment as l, routeGrade as m, evaluateTries as n, gradeSpeakingResponse as o, outcomeFromRoutedGrade as p, score as r, gradeReadAloud as s, evaluate as t, diffDictationChars as u, gradeFromRubric as v, contentHash as w, calibrationReport as x, hasGrade as y };
2425
2457
 
2426
- //# sourceMappingURL=scoring-BGKdff2i.js.map
2458
+ //# sourceMappingURL=scoring-hMsEMhCU.js.map