@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.
@@ -223,16 +223,27 @@ const ROUNDING_MODES = [
223
223
  const MAX_ROUNDING_DP = 15;
224
224
  /**
225
225
  * The rounding policy `value` holds, or `undefined` for none (`undefined` or
226
- * `null`). A malformed one would reach {@link gte} as `10 ** undefined` and
227
- * turn a perfect score into a fail with no error, so it throws here instead.
226
+ * `null`): the check for an optional `rounding`. Anything else is checked by
227
+ * {@link requireRoundingPolicy}.
228
228
  *
229
- * Internal. Every function that takes a `rounding` option checks it here, so
230
- * each refuses the same policies with the same message; `scoring/index.ts`
231
- * does not re-export it.
229
+ * Internal, as is {@link requireRoundingPolicy}: `scoring/index.ts` re-exports
230
+ * neither.
232
231
  */
233
232
  function roundingPolicyOf(value) {
234
- if (value === void 0 || value === null) return;
235
- const { mode, dp } = value;
233
+ return value === void 0 || value === null ? void 0 : requireRoundingPolicy(value);
234
+ }
235
+ /**
236
+ * The rounding policy `value` holds. A malformed one would reach the rounding
237
+ * as `10 ** undefined` and turn a perfect score into a fail with no error, so
238
+ * it throws here instead; so does a missing one, `undefined` or `null`, where a
239
+ * policy is required.
240
+ *
241
+ * Every function that takes a policy, as an argument or a `rounding` option,
242
+ * checks it here or through {@link roundingPolicyOf}, so each refuses the same
243
+ * policies with the same message.
244
+ */
245
+ function requireRoundingPolicy(value) {
246
+ const { mode, dp } = value ?? {};
236
247
  if (!ROUNDING_MODES.includes(mode) || typeof dp !== "number" || !Number.isInteger(dp) || dp < 0 || dp > MAX_ROUNDING_DP) {
237
248
  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}`;
238
249
  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} }.`);
@@ -263,8 +274,9 @@ function scaled(value, dp) {
263
274
  * Normalises `-0` to `0` without touching anything else. `-0` JSON-serialises
264
275
  * as `0` but fails `Object.is(-0, 0)`, which surprises a strict-equality
265
276
  * gradebook comparison. Deliberately NOT `|| 0`: that also swallowed `NaN`
266
- * (reachable with an absurd `dp`, where `10 ** dp` overflows to Infinity) and
267
- * turned a visible failure into a plausible-looking grade of zero.
277
+ * (reached with an absurd `dp`, where `10 ** dp` overflows to Infinity, before
278
+ * policies were checked) and turned a visible failure into a plausible-looking
279
+ * grade of zero.
268
280
  */
269
281
  function noNegZero(value) {
270
282
  return Object.is(value, -0) ? 0 : value;
@@ -275,8 +287,15 @@ function noNegZero(value) {
275
287
  * Idempotent for every mode: rounding an already-rounded value returns it
276
288
  * unchanged. That matters because a composed score is rounded once and then
277
289
  * compared through {@link gte}, which rounds again.
290
+ *
291
+ * @throws RangeError when `policy` is not a {@link RoundingPolicy} — for a
292
+ * non-finite `value` too, which a policy returns unchanged.
278
293
  */
279
294
  function roundGrade(value, policy) {
295
+ return round(value, requireRoundingPolicy(policy));
296
+ }
297
+ /** {@link roundGrade} under a policy {@link requireRoundingPolicy} returned. */
298
+ function round(value, policy) {
280
299
  if (!Number.isFinite(value)) return value;
281
300
  const factor = 10 ** policy.dp;
282
301
  const raw = scaled(value, policy.dp);
@@ -307,9 +326,13 @@ function halfAway(raw) {
307
326
  * shown one number and recorded against another. Rounding both sides — and
308
327
  * allowing an epsilon — makes "what the learner sees" and "what the gradebook
309
328
  * decides" the same comparison.
329
+ *
330
+ * @throws RangeError when `policy` is not a {@link RoundingPolicy}. It is read
331
+ * once, so both sides are rounded under the same one.
310
332
  */
311
333
  function gte(value, threshold, policy) {
312
- return roundGrade(value, policy) >= roundGrade(threshold, policy) - EPSILON;
334
+ const checked = requireRoundingPolicy(policy);
335
+ return round(value, checked) >= round(threshold, checked) - EPSILON;
313
336
  }
314
337
  /**
315
338
  * Classifies a value into a band by **flooring**: the highest band whose `min`
@@ -338,10 +361,16 @@ const DEFAULT_PASS_THRESHOLD = .7;
338
361
  * the rounding band, and this SDK does not alter historical grades without an
339
362
  * explicit decision. Absent, the comparison is the exact raw `>=` it has
340
363
  * always been.
364
+ *
365
+ * The policy is checked at the call, as `score()`'s `rounding` option is:
366
+ * `null` reads as no policy, and one that cannot be applied — an unknown
367
+ * `mode`, or a `dp` that is not a whole number from 0 to 15 — throws a
368
+ * `RangeError` instead of quietly failing the comparison.
341
369
  */
342
370
  function computePassThreshold(activityData, score, rounding) {
371
+ const policy = roundingPolicyOf(rounding);
343
372
  const threshold = activityData.passThreshold ?? .7;
344
- return rounding === void 0 ? score >= threshold : gte(score, threshold, rounding);
373
+ return policy === void 0 ? score >= threshold : gte(score, threshold, policy);
345
374
  }
346
375
  /**
347
376
  * Selects the authored overall feedback for a result: `feedback.correct` when
@@ -1162,8 +1191,11 @@ function rejectedField(slotIds) {
1162
1191
  * until every item has a grade. An outcome whose score cannot be a grade out of
1163
1192
  * its `maxScore` is not a grade either: it is held the same way and named in
1164
1193
  * `rejectedSlotIds`.
1194
+ *
1195
+ * @throws RangeError when `policy.rounding` is not a `RoundingPolicy`.
1165
1196
  */
1166
1197
  function composeAssessmentScore(sections, policy) {
1198
+ const rounding = requireRoundingPolicy(policy.rounding);
1167
1199
  const pendingAll = [];
1168
1200
  const rejectedAll = [];
1169
1201
  const unscorableAll = [];
@@ -1201,7 +1233,7 @@ function composeAssessmentScore(sections, policy) {
1201
1233
  earnedPoints,
1202
1234
  gradedMaxPoints,
1203
1235
  maxPoints,
1204
- score: roundGrade(raw, policy.rounding),
1236
+ score: roundGrade(raw, rounding),
1205
1237
  appliedThreshold,
1206
1238
  pendingSlotIds,
1207
1239
  rejectedSlotIds,
@@ -1218,13 +1250,13 @@ function composeAssessmentScore(sections, policy) {
1218
1250
  gradedMaxPoints: partial.gradedMaxPoints,
1219
1251
  maxPoints: partial.maxPoints,
1220
1252
  score: partial.score,
1221
- passed: partial.appliedThreshold === null || partial.gradedMaxPoints === 0 ? true : gte(partial.score, partial.appliedThreshold, policy.rounding),
1253
+ passed: partial.appliedThreshold === null || partial.gradedMaxPoints === 0 ? true : gte(partial.score, partial.appliedThreshold, rounding),
1222
1254
  appliedThreshold: partial.appliedThreshold,
1223
1255
  pendingSlotIds: partial.pendingSlotIds,
1224
1256
  ...rejectedField(partial.rejectedSlotIds),
1225
1257
  unscorableSlotIds: partial.unscorableSlotIds
1226
1258
  }));
1227
- const score = roundGrade(scored.reduce((sum, section) => sum + section.score * section.normalizedWeight, 0), policy.rounding);
1259
+ const score = roundGrade(scored.reduce((sum, section) => sum + section.score * section.normalizedWeight, 0), rounding);
1228
1260
  const status = pendingAll.length > 0 ? "provisional" : "final";
1229
1261
  if (status === "provisional") return {
1230
1262
  sections: scored,
@@ -1246,7 +1278,7 @@ function composeAssessmentScore(sections, policy) {
1246
1278
  ...rejectedField(rejectedAll),
1247
1279
  unscorableSlotIds: unscorableAll
1248
1280
  };
1249
- const overallOk = gte(score, policy.passThreshold, policy.rounding);
1281
+ const overallOk = gte(score, policy.passThreshold, rounding);
1250
1282
  const sectionsOk = scored.every((section) => section.passed);
1251
1283
  const passed = overallOk && sectionsOk;
1252
1284
  return {
@@ -2639,4 +2671,4 @@ Object.defineProperty(exports, "validateSpeechAssessment", {
2639
2671
  }
2640
2672
  });
2641
2673
 
2642
- //# sourceMappingURL=scoring-Db1CZeAn.cjs.map
2674
+ //# sourceMappingURL=scoring-Bmfu8RS5.cjs.map