@intellectif/lk-core 0.7.1 → 0.8.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 +51 -0
- package/README.md +2 -0
- package/dist/{activity-DARHcrz_.d.cts → activity-CPtJUBek.d.cts} +65 -2
- package/dist/{activity-DARHcrz_.d.ts → activity-CPtJUBek.d.ts} +65 -2
- package/dist/{chunk-MVZKEWZN.cjs → chunk-4VH7RUX6.cjs} +8 -8
- package/dist/{chunk-MVZKEWZN.cjs.map → chunk-4VH7RUX6.cjs.map} +1 -1
- package/dist/{chunk-AFIQQADJ.cjs → chunk-64NBQCRE.cjs} +4 -4
- package/dist/{chunk-AFIQQADJ.cjs.map → chunk-64NBQCRE.cjs.map} +1 -1
- package/dist/{chunk-XL75ZZVM.js → chunk-H7L57ITR.js} +10 -6
- package/dist/chunk-H7L57ITR.js.map +1 -0
- package/dist/{chunk-NOBJYDOB.js → chunk-HTMRJ2SS.js} +2 -2
- package/dist/{chunk-7NIH5IL3.cjs → chunk-JFSZTWHL.cjs} +19 -15
- package/dist/chunk-JFSZTWHL.cjs.map +1 -0
- package/dist/{chunk-YCXIHFLG.js → chunk-OJXTXOUA.js} +87 -5
- package/dist/chunk-OJXTXOUA.js.map +1 -0
- package/dist/{chunk-5RE3ZRSA.cjs → chunk-RHJ5VNFB.cjs} +94 -12
- package/dist/chunk-RHJ5VNFB.cjs.map +1 -0
- package/dist/{chunk-2SQ75JTE.js → chunk-ZBCKQBAB.js} +2 -2
- package/dist/{index-yrUw7NzZ.d.ts → index-BxW_Ho_t.d.cts} +351 -7
- package/dist/{index-BfLwCMXN.d.cts → index-Kc53zVgY.d.ts} +351 -7
- package/dist/index.cjs +173 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -5
- package/dist/index.d.ts +173 -5
- package/dist/index.js +153 -6
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +9 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +8 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +2 -1
- package/dist/chunk-5RE3ZRSA.cjs.map +0 -1
- package/dist/chunk-7NIH5IL3.cjs.map +0 -1
- package/dist/chunk-XL75ZZVM.js.map +0 -1
- package/dist/chunk-YCXIHFLG.js.map +0 -1
- /package/dist/{chunk-NOBJYDOB.js.map → chunk-HTMRJ2SS.js.map} +0 -0
- /package/dist/{chunk-2SQ75JTE.js.map → chunk-ZBCKQBAB.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @intellectif/lk-core
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 011e0d1: Media playback policy — a listening paper can now be played on the paper's terms, and two exam-integrity guards that were watching the wrong doors are closed.
|
|
8
|
+
|
|
9
|
+
Listening assessment was the last thing in v0.4 that could not be run through the SDK at all. An integrating application had replaced the SDK's audio rendering wholesale with a bespoke 56-line player, and the reason was capability, not styling: `<audio controls>` offers a scrubber, a speed control and a download, and a listening paper needs none of them.
|
|
10
|
+
|
|
11
|
+
**`media.playback`, on audio.** `{ maxPlays, seek, rate, controls, nativeControlHints }`, every field optional and every default reproducing the pre-0.8.0 behaviour byte for byte. `{ maxPlays: 2 }` alone is a complete policy: `controls` resolves to `minimal` and `seek` to `none`, because those are the only values that can keep the promise. The browser's own bar leaves its play button enabled after a budget is spent and keeps a scrubber that would move and silently snap back — a control that looks operable and does nothing (WCAG 3.2.2 / 4.1.3). So an enforcing policy replaces the bar rather than lying with it, and the schema **refuses** the combinations that would: `controls: 'native'` with a budget, `maxPlays` with `seek: 'allow'` (scrubbing back would replay the whole recording for free), a bare `minimal` with nothing to enforce, and hints that an enforcing policy would render inert. `playback` on `video`, `image` or `embed` is rejected outright, with the reason: a provider iframe needs the provider's own JS API, and a video transport would have to own fullscreen and Picture-in-Picture, which this release does not.
|
|
12
|
+
|
|
13
|
+
The policy object is **strict** while `media` stays loose. A typo one level down (`maxPlay`, `seeking`) would otherwise be accepted in silence and disarm the whole control on an exam that believed it was enforced.
|
|
14
|
+
|
|
15
|
+
**What replaces the browser's bar is a superset, minus what the policy removes on purpose.** Play/pause, elapsed and total time, mute, volume, an optional speed control, an optional scrubber, and a live plays-remaining status. Volume and mute are never restricted by any policy — they affect no assessment property, and a learner in a lab with a locked OS volume has no other lever once the native bar is gone. An exhausted play button uses `aria-disabled`, never `disabled`: a natively disabled button leaves the focus order, so a learner who tabs into nothing is told nothing, and disabling it mid-attempt would drop focus to `<body>`. Pressing it re-announces the refusal. Every string is overridable through `mediaStrings`, because "No plays remaining" is the highest-stakes text on a listening paper and shipping it as untranslatable English inside a Spanish panel was not acceptable. `review` mode never enforces: a graded paper cannot be changed by listening again, and taking away a learner's scrubber and speed control while they work out what they got wrong helps nobody.
|
|
16
|
+
|
|
17
|
+
**Honest about what a browser actually gives.** `maxPlays`, `seek: 'none'` and `rate: 'fixed'` are _enforced_: a refused play is stopped inside the browser's own `play` event, verified in a real engine at `currentTime < 0.25s`, and the budget lives on the element's event rather than on the button so a hardware media key or a scripted `play()` goes through it too. `nativeControlHints` is _advisory_: it emits `controlsList`, which some engines ignore entirely, and `hide-download` **never prevents a download** — the URL is in the page and the bytes are in the network panel. Nothing here survives devtools. Those are not adjectives; `packages/lk-react/e2e/media-capability-probe.spec.ts` exercises each mechanism in a real browser and runs in CI, so no documented claim outlives the behaviour it rests on.
|
|
18
|
+
|
|
19
|
+
**A budget is only as durable as your write.** The SDK reads and writes no store, so `maxPlays` means nothing across a refresh unless the consumer persists it. `mediaBudget.onPlayConsumed` is a payment, not telemetry: return a promise and playback is held until an atomic server write settles — the only tier in which "consumed before audible" is true of storage rather than only of memory, and the only construction that catches a second tab, since two mounts seeded at 0 both claim 1. Return nothing and playback starts immediately, which is fine for practice — but do not debounce it and do not batch it with the answer autosave; an eight-second debounce is exactly long enough to start a third play and hard-reload. In `exam` mode, budgeted media without `onPlayConsumed` throws at render, and so does a resumed attempt that omits the ledger: a paper granting unlimited plays while showing "2 plays remaining" is indistinguishable, to the learner and to an appeal, from one that works.
|
|
20
|
+
|
|
21
|
+
`MediaPlayLedger` is a **separate** persisted object, not a field on `AttemptState`. Two reasons, both of which bite in production: it is written on a different cadence, and `restoreAttemptState` rebuilds its output field by field — so a build predating this release would not merely fail to read the counts, it would write the snapshot back without them and erase spent plays across a staged rollout, at exactly the moment they matter. `serializeMediaPlayLedger` / `restoreMediaPlayLedger` bind it to its paper by `planHash` for the same reason `AttemptState` is bound.
|
|
22
|
+
|
|
23
|
+
Budgets are frozen into the plan by `planAttempt`, so editing `maxPlays: 2 → 4` mid-window cannot change what a past learner was held to. `planAttempt` also refuses a paper where one recording is budgeted under several keys: six questions each carrying the same clip at `maxPlays: 2` is twelve plays of one recording. Questions that share a recording belong in an item group — one stimulus, one budget, keyed by the entry so a six-question group gets one budget rather than six.
|
|
24
|
+
|
|
25
|
+
**`redact()` had a fail-open hole at exactly this nesting level, and it is fixed first.** `media` was classified as a single `public` leaf, so the whole object was forwarded without recursing — and an unclassified key nested under it survived `redact()` **and passed `assertRedacted()`**. A hand-planted `media.secretAnswerHint` reached the learner on 0.7.1. The documented fail-closed contract ("a field the policy does not classify is removed") stopped at the media boundary; it no longer does. Unknown keys under `media` are now dropped and rejected, `RedactedMediaSchema` is strict like the shapes that embed it, and `redact()` returns a copy rather than an alias. Verified against the published 0.7.1 build: every existing paper plans to a byte-identical `planHash` and redacts to identical output.
|
|
26
|
+
|
|
27
|
+
**Two exam-integrity guards that were watching the wrong doors:**
|
|
28
|
+
|
|
29
|
+
- **An unseeded item-level shuffle rendered happily in `exam` mode.** `ActivitySequence` demanded a `shuffleSeed` for `shuffle="entries"` and for a group's `shuffle: 'within-group'`, but never for an activity's own `data.shuffle` — and `<MultipleChoice>` invents a per-mount seed when none reaches it, in every render mode. A single item with `shuffle: true` produced four different option orders across four mounts, without complaint: an arrangement the server cannot rebuild, which is precisely what the guard exists to refuse, arriving through the one door it did not watch.
|
|
30
|
+
- **`<WrittenResponse>` had no redacted-in-`practice` guard.** Its two siblings have refused redacted data there since 0.5.0. The asymmetry looked harmless because this component never scores locally — but `practice` still runs the local submit path and emits a practice-mode xAPI statement for work the server is meant to grade, so an all-essay redacted paper mounted without `renderMode` rendered, stayed answerable, and reported success at every step. The one item type whose grading is entirely someone else's job was the one that failed silently.
|
|
31
|
+
|
|
32
|
+
Both now throw at render. That is the breaking half of this release for anyone who was relying on the old behaviour — though what they were relying on was an exam whose order could not be reproduced, or an essay graded nowhere.
|
|
33
|
+
|
|
34
|
+
**On `maxPlays` specifically, and stated plainly because this is a public changelog:** it is a roadmap hypothesis, not a replicated production behaviour. A search of the one integrating application for any play-count identifier, UI string or database column returned nothing. What it had actually built was seek, download and rate suppression — which is why those are the parts with a real usage story behind them, and why `maxPlays` ships with its persistence requirements spelled out rather than assumed.
|
|
35
|
+
|
|
36
|
+
**A blank left empty could score as correct.** Levenshtein distance from an empty string is just the answer's length, so `levenshtein: 1` on a one-letter blank — an article, or "I" — accepted an **unanswered** blank: `matchText('', ['a'], { levenshtein: 1 })` returned `{ matched: true, via: 'fuzzy' }`. On a partial-credit gap-fill that silently awarded a mark for work the learner never did, and no test covered the path. An empty or whitespace-only input no longer reaches the fuzzy stage at all. Exact and normalized matching are untouched, so an author who deliberately lists `""` as an accepted answer still gets it, one stage earlier.
|
|
37
|
+
|
|
38
|
+
**This can change a recorded grade** — for exactly one population: attempts where an unanswered blank was marked correct by fuzzy tolerance. Every one of those marks was wrong. If you have stored gap-fill grades under a `levenshtein` policy with very short accepted answers, they are worth recomputing. Related hazard, unchanged and now documented rather than silently fixed: `levenshtein: 1` against a one-character answer still accepts _any_ single character, because that is literally what an edit distance of 1 means. Prefer exact matching on single-letter blanks.
|
|
39
|
+
|
|
40
|
+
**Three skin bugs in the post-submit marking**, all reported by a consumer against 6.1.0 and all reproduced by computed style in a real engine before being fixed:
|
|
41
|
+
|
|
42
|
+
- **A correctly chosen option kept the selection colour.** Specificity, not order: `.lk-mc-option:has(input:checked)` is (0,2,1) and outranked `.lk-mc-option[data-correct="true"]` at (0,2,0), so the single most reassuring state in the component — "you picked this and it is right" — was the only one that did not mark. The wrong-and-chosen case worked only because someone had written the combined selector for it; this adds its missing counterpart.
|
|
43
|
+
- **Hovering a marked option repainted it in the selection colour.** `.lk-mc-option:hover` was unguarded, and for an unchosen distractor no marking rule matches at all — so after submit, hovering a disabled, already-marked option showed the "still selectable" affordance. Hover is now scoped to `:not([data-correct])`, which is precisely "not yet marked".
|
|
44
|
+
- **Option text wrapped under a lonely radio on narrow screens.** The option is a flex row and consumers commonly add `flex-wrap: wrap` so a per-option note can drop below it; in a wrapping container an item wraps _before_ it shrinks, and the text span had no flex contract. It now has one, with a minimum inline size so a wide sibling wraps instead of crushing the text into a one-character-per-line column.
|
|
45
|
+
|
|
46
|
+
`e2e/skin-marking.spec.ts` pins all three by **computed border colour and box geometry** in a real browser. Unit tests assert class names and `data-*` attributes and can never see which rule wins, which is why this class of bug survived several releases.
|
|
47
|
+
|
|
48
|
+
Fixing the first of those exposed a design gap the fix alone would have left in place: once a correctly chosen option marked green, it became indistinguishable from the correct answer a learner **missed**, which also marks green. The skin now separates them by weight rather than hue — chosen-and-right is solid with a tint and a `✓`, chosen-and-wrong is solid red with a `✗`, and the missed correct answer is a **dashed** green outline with a hollow `○` and no fill. Untouched distractors recede to 70% opacity, a figure chosen by computing the contrast (6.6:1; the first draft's 55% would have failed AA at 3.9:1). The glyphs also close a WCAG 1.4.1 gap: the component emits no correctness text, so a mark was previously carried by colour alone. They are decorative generated content on purpose — a translatable announcement belongs to the component and is tracked with the i18n work.
|
|
49
|
+
|
|
50
|
+
Also in this release: `slotKey` containing a `.` is now rejected at `validateItemGroup` rather than at `flattenSequence`, so an author finds it instead of a learner; `InteractionKind` gains `media-play-consumed` / `-refused` / `-errored` / `-refunded`, emitted only when a budget is in force so a `review` replay cannot pollute the record an appeal reads; and the pager's stimulus pane now keys on the set as well as the entry, so a set change to a different paper that reuses an entry key remounts the panel instead of leaking a mount-only seed into the next paper.
|
|
51
|
+
|
|
52
|
+
`@intellectif/lk-react` goes major because `@intellectif/lk-core` is a peer dependency and a core minor raises the floor, and because of the two new throws. Every new prop is optional.
|
|
53
|
+
|
|
3
54
|
## 0.7.1
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ npm install @intellectif/lk-core
|
|
|
25
25
|
- **Item groups** — `Stimulus` + `ItemGroup` (one passage or recording serving several questions), `flattenSequence()` for seeded, shuffle-atomic presentation order with stable slot identity. Slot ids are positional by default (`"3"`, `"1.0"`); author a `slotKey` on an entry and its id survives an insert, so a stored response cannot slide onto a different question.
|
|
26
26
|
- **Attempt plans** — `planAttempt(entries, { seed, shuffleEntries, points })` freezes the exact paper a learner sat: slot order, per-slot points, a content hash of each item, and a `planHash` over the lot. `verifyAttemptPlan(plan, current)` reports the drift when that content is later edited (`changedSlotIds`, `changedStimulusSlotIds`, `changedPointsSlotIds`, `missingSlotIds`, …) — the thing you need during a remark or an appeal. `scoredItemsFromPlan(plan, outcomes)` turns collected outcomes into the `ScoredItem[]` composition wants, and defaults a slot with **no** outcome to `deferred` rather than silently shrinking the denominator.
|
|
27
27
|
- **Attempt state** — `serializeAttemptState(plan, progress)` / `restoreAttemptState(plan, snapshot)` round-trip an interrupted attempt (responses, submitted slots, position) and are **bound to the plan**: restoring answers onto a paper the learner never sat throws instead of succeeding quietly. `diffResponses(before, after)` reports what moved between two snapshots, including an answer the learner *cleared*.
|
|
28
|
+
- **Media playback policy** — `media.playback` on an audio recording: `maxPlays`, `seek`, `rate`, and advisory `nativeControlHints`. `resolvePlaybackPolicy()` derives the defaults once so the schema, the plan and the renderer cannot drift. Budgets are frozen into the plan by `planAttempt`, so editing `maxPlays` mid-window cannot change what a past learner was held to, and `planAttempt` refuses a paper where one recording is budgeted under two keys.
|
|
29
|
+
- **Play ledger** — `serializeMediaPlayLedger` / `restoreMediaPlayLedger` / `planMediaBudgets`, with `slotMediaKey()` / `stimulusMediaKey()` for the keys. Bound to its paper by `planHash`, and deliberately **separate** from `AttemptState`: a play is charged immediately while answers autosave on a debounce, and a build that predates the ledger would rewrite an `AttemptState` snapshot without the counts and erase them.
|
|
28
30
|
- **Content hashing** — `contentHash()` / `canonicalJson()` / `fingerprint()`: deterministic, dependency-free change detection (not a tamper-evident signature).
|
|
29
31
|
- **`redact()` / `assertRedacted()`** — fail-closed, policy-driven projections that are safe to send to an exam client, with per-type redacted TypeScript types derived from the strict schemas.
|
|
30
32
|
- **`redactItemGroup()` / `assertRedactedItemGroup()`** — the container equivalent, and **not optional**: `redact()` takes a single activity, so mapping a group's items through it and shipping the container leaves `Stimulus.transcript` — the author-only transcript of a listening passage, i.e. the answers — in the payload. Redact a group with `redactItemGroup()`.
|
|
@@ -383,6 +383,57 @@ interface ActivityMedia {
|
|
|
383
383
|
alt?: string;
|
|
384
384
|
/** Optional WebVTT captions track URL for `audio`/`video`. */
|
|
385
385
|
captionsUrl?: string;
|
|
386
|
+
/**
|
|
387
|
+
* How the recording may be played. `audio` only — see
|
|
388
|
+
* {@link MediaPlaybackPolicy}. Absent means today's behaviour exactly: the
|
|
389
|
+
* browser's own control bar, unlimited plays, free seeking and free speed.
|
|
390
|
+
*/
|
|
391
|
+
playback?: MediaPlaybackPolicy;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A hint to the browser's own control bar, emitted as a `controlsList` token.
|
|
395
|
+
*
|
|
396
|
+
* Advisory, and engine-dependent. `hide-download` removes a menu item where
|
|
397
|
+
* `controlsList` is implemented; it never prevents a download, because the URL
|
|
398
|
+
* is in the page and the bytes are in the network panel. The real control is a
|
|
399
|
+
* short-lived signed URL, which belongs to the consuming application.
|
|
400
|
+
*/
|
|
401
|
+
type NativeControlHint = 'hide-download' | 'hide-rate';
|
|
402
|
+
/**
|
|
403
|
+
* How an audio recording may be played.
|
|
404
|
+
*
|
|
405
|
+
* Every field is optional and every default reproduces the SDK's pre-0.8.0
|
|
406
|
+
* behaviour, so adding this key is the only thing that changes anything.
|
|
407
|
+
*
|
|
408
|
+
* Setting any *enforcement* field (`maxPlays`, `seek: 'none'`, `rate: 'fixed'`)
|
|
409
|
+
* resolves `controls` to `'minimal'`: the browser's bar cannot express a spent
|
|
410
|
+
* budget — its play button stays enabled — so the SDK renders its own
|
|
411
|
+
* transport rather than leave a control that looks operable and does nothing.
|
|
412
|
+
*/
|
|
413
|
+
interface MediaPlaybackPolicy {
|
|
414
|
+
/**
|
|
415
|
+
* `native` renders the browser's control bar. `minimal` renders the SDK
|
|
416
|
+
* transport. Resolved automatically; set it explicitly only to keep the
|
|
417
|
+
* native bar on a recording that has nothing to enforce.
|
|
418
|
+
*/
|
|
419
|
+
controls?: 'native' | 'minimal';
|
|
420
|
+
/**
|
|
421
|
+
* How many times the recording may be STARTED (1–20). A play is consumed
|
|
422
|
+
* when playback begins from anywhere other than where it last stopped, so
|
|
423
|
+
* pausing, resuming, and paging between the questions of one listening group
|
|
424
|
+
* are free.
|
|
425
|
+
*
|
|
426
|
+
* The SDK refuses the play; it does not remember it. The count is durable
|
|
427
|
+
* only if the consuming application persists it — see `mediaBudget` on
|
|
428
|
+
* `<ActivitySequence>` in `@intellectif/lk-react`.
|
|
429
|
+
*/
|
|
430
|
+
maxPlays?: number;
|
|
431
|
+
/** `none` renders no scrubber and reverts an out-of-band seek. Resolves to `none` under a budget. */
|
|
432
|
+
seek?: 'allow' | 'none';
|
|
433
|
+
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
434
|
+
rate?: 'allow' | 'fixed';
|
|
435
|
+
/** Advisory hints to the native bar. See {@link NativeControlHint}. */
|
|
436
|
+
nativeControlHints?: NativeControlHint[];
|
|
386
437
|
}
|
|
387
438
|
/**
|
|
388
439
|
* Optional authored "overall feedback" shown after submission, chosen by
|
|
@@ -769,7 +820,19 @@ interface ActivityResult {
|
|
|
769
820
|
* registered by consumers may emit their own kinds, so any string is accepted;
|
|
770
821
|
* the named literals are kept for autocompletion.
|
|
771
822
|
*/
|
|
772
|
-
type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'text-changed' | 'submitted'
|
|
823
|
+
type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'text-changed' | 'submitted'
|
|
824
|
+
/**
|
|
825
|
+
* A play was charged against a recording's budget, before any audio was
|
|
826
|
+
* audible. Emitted only when a budget is in force, so a `review`-mode replay
|
|
827
|
+
* never pollutes the record an appeal reads.
|
|
828
|
+
*/
|
|
829
|
+
| 'media-play-consumed'
|
|
830
|
+
/** An exhausted budget refused a play. The artifact an appeal asks for. */
|
|
831
|
+
| 'media-play-refused'
|
|
832
|
+
/** A charged play produced no audio — a failed or expired media URL. */
|
|
833
|
+
| 'media-play-errored'
|
|
834
|
+
/** A charged play was returned, because the consumer accepted the refund. */
|
|
835
|
+
| 'media-play-refunded' | (string & {});
|
|
773
836
|
/** Fired by activity components on every discrete learner interaction. */
|
|
774
837
|
interface InteractionEvent {
|
|
775
838
|
/** The kind of interaction that occurred. */
|
|
@@ -782,4 +845,4 @@ interface InteractionEvent {
|
|
|
782
845
|
payload: Record<string, unknown>;
|
|
783
846
|
}
|
|
784
847
|
|
|
785
|
-
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type
|
|
848
|
+
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIStatement as O, type XAPIVerbObject as P, levenshteinDistance as Q, matchText as R, type ScoringDetail as S, type TextMatchPolicy as T, type ValidationError as V, type WrittenResponseData as W, type XAPIActor as X, type ActivityDataMap as a, type ActivityFeedback as b, type ActivityMedia as c, type ActivityResult as d, type ActivityType as e, type FillInTheBlanksLearnerResponse as f, type Grader as g, type GraderKind as h, type GraderUsage as i, type GradingState as j, type InteractionEvent as k, type InteractionKind as l, type ItemOutcome as m, type LearnerResponseMap as n, type MultipleChoiceLearnerResponse as o, type MultipleChoiceOption as p, type ScoringOutcome as q, type ScoringResult as r, type TextMatchResult as s, type ValidationResult as t, type WrittenResponseLearnerResponse as u, type WrittenResponseRubric as v, type WrittenResponseRubricCriterion as w, type XAPIConfig as x, type XAPIContext as y, type XAPIContextActivities as z };
|
|
@@ -383,6 +383,57 @@ interface ActivityMedia {
|
|
|
383
383
|
alt?: string;
|
|
384
384
|
/** Optional WebVTT captions track URL for `audio`/`video`. */
|
|
385
385
|
captionsUrl?: string;
|
|
386
|
+
/**
|
|
387
|
+
* How the recording may be played. `audio` only — see
|
|
388
|
+
* {@link MediaPlaybackPolicy}. Absent means today's behaviour exactly: the
|
|
389
|
+
* browser's own control bar, unlimited plays, free seeking and free speed.
|
|
390
|
+
*/
|
|
391
|
+
playback?: MediaPlaybackPolicy;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A hint to the browser's own control bar, emitted as a `controlsList` token.
|
|
395
|
+
*
|
|
396
|
+
* Advisory, and engine-dependent. `hide-download` removes a menu item where
|
|
397
|
+
* `controlsList` is implemented; it never prevents a download, because the URL
|
|
398
|
+
* is in the page and the bytes are in the network panel. The real control is a
|
|
399
|
+
* short-lived signed URL, which belongs to the consuming application.
|
|
400
|
+
*/
|
|
401
|
+
type NativeControlHint = 'hide-download' | 'hide-rate';
|
|
402
|
+
/**
|
|
403
|
+
* How an audio recording may be played.
|
|
404
|
+
*
|
|
405
|
+
* Every field is optional and every default reproduces the SDK's pre-0.8.0
|
|
406
|
+
* behaviour, so adding this key is the only thing that changes anything.
|
|
407
|
+
*
|
|
408
|
+
* Setting any *enforcement* field (`maxPlays`, `seek: 'none'`, `rate: 'fixed'`)
|
|
409
|
+
* resolves `controls` to `'minimal'`: the browser's bar cannot express a spent
|
|
410
|
+
* budget — its play button stays enabled — so the SDK renders its own
|
|
411
|
+
* transport rather than leave a control that looks operable and does nothing.
|
|
412
|
+
*/
|
|
413
|
+
interface MediaPlaybackPolicy {
|
|
414
|
+
/**
|
|
415
|
+
* `native` renders the browser's control bar. `minimal` renders the SDK
|
|
416
|
+
* transport. Resolved automatically; set it explicitly only to keep the
|
|
417
|
+
* native bar on a recording that has nothing to enforce.
|
|
418
|
+
*/
|
|
419
|
+
controls?: 'native' | 'minimal';
|
|
420
|
+
/**
|
|
421
|
+
* How many times the recording may be STARTED (1–20). A play is consumed
|
|
422
|
+
* when playback begins from anywhere other than where it last stopped, so
|
|
423
|
+
* pausing, resuming, and paging between the questions of one listening group
|
|
424
|
+
* are free.
|
|
425
|
+
*
|
|
426
|
+
* The SDK refuses the play; it does not remember it. The count is durable
|
|
427
|
+
* only if the consuming application persists it — see `mediaBudget` on
|
|
428
|
+
* `<ActivitySequence>` in `@intellectif/lk-react`.
|
|
429
|
+
*/
|
|
430
|
+
maxPlays?: number;
|
|
431
|
+
/** `none` renders no scrubber and reverts an out-of-band seek. Resolves to `none` under a budget. */
|
|
432
|
+
seek?: 'allow' | 'none';
|
|
433
|
+
/** `fixed` renders no speed control and snaps `playbackRate` back to 1. */
|
|
434
|
+
rate?: 'allow' | 'fixed';
|
|
435
|
+
/** Advisory hints to the native bar. See {@link NativeControlHint}. */
|
|
436
|
+
nativeControlHints?: NativeControlHint[];
|
|
386
437
|
}
|
|
387
438
|
/**
|
|
388
439
|
* Optional authored "overall feedback" shown after submission, chosen by
|
|
@@ -769,7 +820,19 @@ interface ActivityResult {
|
|
|
769
820
|
* registered by consumers may emit their own kinds, so any string is accepted;
|
|
770
821
|
* the named literals are kept for autocompletion.
|
|
771
822
|
*/
|
|
772
|
-
type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'text-changed' | 'submitted'
|
|
823
|
+
type InteractionKind = 'option-selected' | 'option-deselected' | 'blank-filled' | 'hint-requested' | 'text-changed' | 'submitted'
|
|
824
|
+
/**
|
|
825
|
+
* A play was charged against a recording's budget, before any audio was
|
|
826
|
+
* audible. Emitted only when a budget is in force, so a `review`-mode replay
|
|
827
|
+
* never pollutes the record an appeal reads.
|
|
828
|
+
*/
|
|
829
|
+
| 'media-play-consumed'
|
|
830
|
+
/** An exhausted budget refused a play. The artifact an appeal asks for. */
|
|
831
|
+
| 'media-play-refused'
|
|
832
|
+
/** A charged play produced no audio — a failed or expired media URL. */
|
|
833
|
+
| 'media-play-errored'
|
|
834
|
+
/** A charged play was returned, because the consumer accepted the refund. */
|
|
835
|
+
| 'media-play-refunded' | (string & {});
|
|
773
836
|
/** Fired by activity components on every discrete learner interaction. */
|
|
774
837
|
interface InteractionEvent {
|
|
775
838
|
/** The kind of interaction that occurred. */
|
|
@@ -782,4 +845,4 @@ interface InteractionEvent {
|
|
|
782
845
|
payload: Record<string, unknown>;
|
|
783
846
|
}
|
|
784
847
|
|
|
785
|
-
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type
|
|
848
|
+
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIStatement as O, type XAPIVerbObject as P, levenshteinDistance as Q, matchText as R, type ScoringDetail as S, type TextMatchPolicy as T, type ValidationError as V, type WrittenResponseData as W, type XAPIActor as X, type ActivityDataMap as a, type ActivityFeedback as b, type ActivityMedia as c, type ActivityResult as d, type ActivityType as e, type FillInTheBlanksLearnerResponse as f, type Grader as g, type GraderKind as h, type GraderUsage as i, type GradingState as j, type InteractionEvent as k, type InteractionKind as l, type ItemOutcome as m, type LearnerResponseMap as n, type MultipleChoiceLearnerResponse as o, type MultipleChoiceOption as p, type ScoringOutcome as q, type ScoringResult as r, type TextMatchResult as s, type ValidationResult as t, type WrittenResponseLearnerResponse as u, type WrittenResponseRubric as v, type WrittenResponseRubricCriterion as w, type XAPIConfig as x, type XAPIContext as y, type XAPIContextActivities as z };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkRHJ5VNFBcjs = require('./chunk-RHJ5VNFB.cjs');
|
|
7
7
|
|
|
8
8
|
// src/scoring/rounding.ts
|
|
9
9
|
var EPSILON = 1e-9;
|
|
@@ -265,26 +265,26 @@ function selectFeedback(activityData, passed) {
|
|
|
265
265
|
return _nullishCoalesce((passed ? feedback.correct : feedback.incorrect), () => ( null));
|
|
266
266
|
}
|
|
267
267
|
function score(activityType, activityData, learnerResponse) {
|
|
268
|
-
const descriptor =
|
|
268
|
+
const descriptor = _chunkRHJ5VNFBcjs.getActivityTypeDescriptor.call(void 0, activityType);
|
|
269
269
|
if (descriptor === void 0) {
|
|
270
|
-
throw new (0,
|
|
270
|
+
throw new (0, _chunkRHJ5VNFBcjs.UnknownActivityTypeError)(String(activityType));
|
|
271
271
|
}
|
|
272
272
|
if (descriptor.scoring.kind === "deferred") {
|
|
273
|
-
throw new (0,
|
|
273
|
+
throw new (0, _chunkRHJ5VNFBcjs.DeferredScoringError)(descriptor.type);
|
|
274
274
|
}
|
|
275
275
|
if (isRedacted(activityData)) {
|
|
276
|
-
throw new (0,
|
|
276
|
+
throw new (0, _chunkRHJ5VNFBcjs.RedactedScoringError)(descriptor.type);
|
|
277
277
|
}
|
|
278
278
|
const result = descriptor.scoring.score(activityData, learnerResponse);
|
|
279
279
|
if (!Number.isFinite(result.score)) {
|
|
280
|
-
throw new (0,
|
|
280
|
+
throw new (0, _chunkRHJ5VNFBcjs.RedactedScoringError)(descriptor.type);
|
|
281
281
|
}
|
|
282
282
|
const passed = computePassThreshold(activityData, result.score);
|
|
283
283
|
return { ...result, passed, feedback: _nullishCoalesce(result.feedback, () => ( selectFeedback(activityData, passed))) };
|
|
284
284
|
}
|
|
285
285
|
function evaluate(data, response) {
|
|
286
286
|
const type = data.type;
|
|
287
|
-
const descriptor = typeof type === "string" ?
|
|
287
|
+
const descriptor = typeof type === "string" ? _chunkRHJ5VNFBcjs.getActivityTypeDescriptor.call(void 0, type) : void 0;
|
|
288
288
|
if (descriptor === void 0) {
|
|
289
289
|
return {
|
|
290
290
|
status: "unscorable",
|
|
@@ -340,4 +340,4 @@ function evaluate(data, response) {
|
|
|
340
340
|
|
|
341
341
|
|
|
342
342
|
exports.roundGrade = roundGrade; exports.gte = gte; exports.classifyBand = classifyBand; exports.composeAssessmentScore = composeAssessmentScore; exports.DEFAULT_PASS_THRESHOLD = DEFAULT_PASS_THRESHOLD; exports.computePassThreshold = computePassThreshold; exports.score = score; exports.evaluate = evaluate; exports.gradeFromRubric = gradeFromRubric; exports.outcomeFromGrade = outcomeFromGrade; exports.hasGrade = hasGrade;
|
|
343
|
-
//# sourceMappingURL=chunk-
|
|
343
|
+
//# sourceMappingURL=chunk-4VH7RUX6.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-MVZKEWZN.cjs","../src/scoring/rounding.ts","../src/grading.ts","../src/scoring/compose.ts","../src/scoring/index.ts"],"names":["EPSILON","score"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-MVZKEWZN.cjs","sourcesContent":[null,"/**\n * Rounding for assessment scores.\n *\n * Rounding is **two different operations** that must not share one policy, and\n * conflating them silently inverts one of them:\n *\n * - **Grade rounding** decides the number a learner is shown and recorded\n * against. It rounds to a fixed number of decimal places, conventionally\n * half-up, so a learner shown \"70%\" is not recorded as a fail at 69.6.\n * - **Band / level classification** decides which level someone is placed in.\n * It deliberately **floors**: placing a learner above their real level is\n * the more harmful error, so a band boundary must not be reached by\n * rounding up.\n *\n * There is therefore no default `RoundingPolicy` anywhere in this SDK, and\n * `dp` has no default either — an integrator's deliberate choice must never be\n * supplied by us.\n */\n\n/** How a value is rounded to `dp` decimal places. */\nexport type RoundingMode = 'half-up' | 'half-even' | 'floor' | 'ceil';\n\n/** A rounding policy. Both fields are required — the SDK never guesses either. */\nexport interface RoundingPolicy {\n mode: RoundingMode;\n /** Decimal places. Load-bearing: `dp: 2` is what stops 69.6 becoming a fail at 70. */\n dp: number;\n}\n\n/**\n * Float-noise guard. Binary floating point makes `1.005 * 100` come out as\n * `100.49999999999999` and `0.29 * 100` come out as `28.999999999999996`;\n * without an allowance both would move a learner's grade a step. It is applied\n * in the direction each mode needs — see {@link roundGrade}.\n */\nconst EPSILON = 1e-9;\n\n/**\n * Tolerance for deciding \"is this exactly halfway?\" in half-even mode.\n * Deliberately much larger than {@link EPSILON}: if the tie test shared the\n * nudge's tolerance, the nudge would decide its own outcome. Safe for grades,\n * which live on a [0,1] scale where representation error is far below this.\n */\nconst TIE_TOLERANCE = 1e-6;\n\nfunction scaled(value: number, dp: number): number {\n return value * 10 ** dp;\n}\n\n/**\n * Normalises `-0` to `0` without touching anything else. `-0` JSON-serialises\n * as `0` but fails `Object.is(-0, 0)`, which surprises a strict-equality\n * gradebook comparison. Deliberately NOT `|| 0`: that also swallowed `NaN`\n * (reachable with an absurd `dp`, where `10 ** dp` overflows to Infinity) and\n * turned a visible failure into a plausible-looking grade of zero.\n */\nfunction noNegZero(value: number): number {\n return Object.is(value, -0) ? 0 : value;\n}\n\n/**\n * Rounds `value` to `policy.dp` places under `policy.mode`.\n *\n * Idempotent for every mode: rounding an already-rounded value returns it\n * unchanged. That matters because a composed score is rounded once and then\n * compared through {@link gte}, which rounds again.\n */\nexport function roundGrade(value: number, policy: RoundingPolicy): number {\n if (!Number.isFinite(value)) {\n return value;\n }\n const factor = 10 ** policy.dp;\n const raw = scaled(value, policy.dp);\n\n // The nudge must oppose the mode, never point uniformly away from zero.\n // `Math.ceil` on a value that is ALREADY exact at `dp` jumps a whole\n // quantum if you add epsilon first: 0.7 became 0.71, a true 0 became 0.01,\n // and the operation stopped being idempotent (0.7 -> 0.71 -> 0.72). So\n // `ceil` nudges down and `floor` nudges up — just far enough to absorb\n // representation error (0.29 * 100 is 28.999999999999996, which must still\n // floor to 29) without ever moving a value that was already exact.\n // Half modes keep the sign-aware nudge, which is what makes a .5 tie land\n // away from zero.\n switch (policy.mode) {\n case 'floor':\n return noNegZero(Math.floor(raw + EPSILON) / factor);\n case 'ceil':\n return noNegZero(Math.ceil(raw - EPSILON) / factor);\n case 'half-even': {\n // Detect the tie on the RAW value, and with a tolerance decoupled from\n // (and far larger than) the nudge epsilon. Testing the NUDGED value\n // against EPSILON let the nudge itself decide whether a tie was seen:\n // 0.375 was detected but 0.125 was not, so half-even silently degraded\n // to half-up for some exact ties — and only ever in the direction that\n // rounds a learner UP, the opposite of what this mode is chosen for.\n const floored = Math.floor(raw);\n if (Math.abs(raw - floored - 0.5) > TIE_TOLERANCE) {\n return noNegZero(halfAway(raw) / factor);\n }\n // Exactly halfway: pick the even neighbour (banker's rounding).\n const even = floored % 2 === 0 ? floored : floored + 1;\n return noNegZero(even / factor);\n }\n default:\n return noNegZero(halfAway(raw) / factor);\n }\n}\n\n/**\n * Half-up on a scaled value, with the sign-aware float-noise nudge.\n * `Math.round` breaks .5 ties toward +Infinity, so negatives are mirrored to\n * keep the tie breaking away from zero in both directions.\n */\nfunction halfAway(raw: number): number {\n const nudged = raw >= 0 ? raw + EPSILON : raw - EPSILON;\n return raw >= 0 ? Math.round(nudged) : -Math.round(-nudged);\n}\n\n/**\n * Threshold comparison that rounds **both sides** before comparing.\n *\n * Comparing a raw float against a rounded threshold is how a learner ends up\n * shown one number and recorded against another. Rounding both sides — and\n * allowing an epsilon — makes \"what the learner sees\" and \"what the gradebook\n * decides\" the same comparison.\n */\nexport function gte(value: number, threshold: number, policy: RoundingPolicy): boolean {\n return roundGrade(value, policy) >= roundGrade(threshold, policy) - EPSILON;\n}\n\n/** A named band with an inclusive lower bound, e.g. `{ name: 'B1', min: 0.6 }`. */\nexport interface Band {\n name: string;\n /** Inclusive lower bound on the same scale as the value being classified. */\n min: number;\n}\n\n/**\n * Classifies a value into a band by **flooring**: the highest band whose `min`\n * the value actually reaches. Deliberately does NOT round up to a boundary —\n * over-placement is the more harmful error, so a learner just below a boundary\n * stays below it.\n *\n * Returns `null` when the value reaches no band's minimum.\n */\nexport function classifyBand(value: number, bands: readonly Band[]): Band | null {\n let best: Band | null = null;\n for (const band of bands) {\n if (value >= band.min - EPSILON && (best === null || band.min > best.min)) {\n best = band;\n }\n }\n return best;\n}\n","import { computePassThreshold } from './scoring/index.js';\nimport type { ActivityData, ItemOutcome } from './types/activity.js';\nimport type { CriterionScore, GradeRecord } from './types/grading.js';\n\nexport type {\n CriterionScore,\n GradeRecord,\n Grader,\n GraderKind,\n GraderUsage,\n GradingState,\n InlineCorrection,\n} from './types/grading.js';\n\n/** Options for {@link gradeFromRubric}. */\nexport interface GradeFromRubricOptions {\n /**\n * Scaled score [0,1] required to pass. Defaults to the activity's\n * `passThreshold`, or {@link DEFAULT_PASS_THRESHOLD} when absent.\n */\n passThreshold?: number;\n /** Narrative feedback for the learner, carried onto the record verbatim. */\n feedback?: string | null;\n}\n\n/**\n * Computes the weighted total from per-criterion scores — the arithmetic that\n * must NOT be delegated to a language model.\n *\n * A grader is asked for judgement (how good is this criterion?), not for\n * mental arithmetic. Asking a model to also produce the weighted total makes\n * the final grade unverifiable and unreproducible: two runs can return\n * different totals for identical criterion scores. This function makes the\n * total a pure function of the judgements, so a grade can be recomputed and\n * audited years later.\n *\n * Weights are normalised by their sum, so they need not add to 1. Criteria\n * marked `notApplicable`, and those carrying no numeric `score` (a purely\n * banded judgement), are excluded from both numerator and denominator. When\n * nothing scoreable remains the result is `unscorable`, never a zero.\n *\n * Scores need not be in [0,1]: set `maxScore` on a criterion to declare what\n * its score is out of, and each is normalised before weighting. A grader\n * working out of 100 says so and is done:\n *\n * ```ts\n * gradeFromRubric([\n * { name: 'Task achievement', score: 82, maxScore: 100, weight: 2 },\n * { name: 'Range', score: 7, maxScore: 9, weight: 1 },\n * ]);\n * ```\n *\n * The returned `GradeRecord.score` is always scaled [0,1] against\n * `maxScore: 1`, like every other score in the SDK.\n */\nexport function gradeFromRubric(\n criteria: readonly CriterionScore[],\n activityData?: ActivityData,\n options: GradeFromRubricOptions = {},\n): GradeRecord | { unscorable: true; reason: string } {\n // A corrupt score is an ERROR, not an exclusion. Exclusions (`notApplicable`,\n // and band-only criteria with no numeric score) are deliberate authoring\n // decisions; a NaN or Infinity is a broken grader. Silently dropping it\n // would regrade the learner on fewer criteria — with different effective\n // weights — and nobody would know.\n const corrupt = criteria.find(\n (criterion) =>\n criterion.notApplicable !== true &&\n criterion.score !== undefined &&\n !Number.isFinite(criterion.score),\n );\n if (corrupt !== undefined) {\n return {\n unscorable: true,\n reason: `Criterion \"${corrupt.name}\" has a non-finite score (${String(corrupt.score)}). A grade cannot be computed from it.`,\n };\n }\n\n // A `maxScore` that is absent means 1 (the scaled convention). One that is\n // present must be a positive finite number: dividing by 0, a negative, or a\n // NaN would produce a grade nobody can defend.\n const badMax = criteria.find(\n (criterion) =>\n criterion.notApplicable !== true &&\n criterion.maxScore !== undefined &&\n !(Number.isFinite(criterion.maxScore) && criterion.maxScore > 0),\n );\n if (badMax !== undefined) {\n return {\n unscorable: true,\n reason: `Criterion \"${badMax.name}\" declares maxScore ${String(badMax.maxScore)}; it must be a positive, finite number.`,\n };\n }\n\n const scoreable = criteria.filter(\n (criterion) => criterion.notApplicable !== true && typeof criterion.score === 'number',\n );\n\n /** The criterion's score as a ratio in [0,1]. `maxScore` defaults to 1. */\n const ratioOf = (criterion: CriterionScore): number =>\n (criterion.score as number) / (criterion.maxScore ?? 1);\n\n // A grader that returns raw points (4 out of 5) without declaring\n // `maxScore: 5` would otherwise yield `{ score: 4, maxScore: 1 }` and pass\n // every threshold. Reject rather than clamp: silently rescaling someone's\n // grader is worse than telling them it is out of contract. A hair outside\n // the range is float noise and is clamped further down.\n const EPSILON = 1e-9;\n const outOfRange = scoreable.find(\n (criterion) => ratioOf(criterion) < -EPSILON || ratioOf(criterion) > 1 + EPSILON,\n );\n if (outOfRange !== undefined) {\n return {\n unscorable: true,\n reason:\n `Criterion \"${outOfRange.name}\" has score ${outOfRange.score} out of ${outOfRange.maxScore ?? 1}, ` +\n 'which is outside the [0,1] range once scaled. Set `maxScore` on the criterion to declare what the ' +\n 'score is out of (e.g. `maxScore: 100` for a 0–100 grader).',\n };\n }\n\n if (scoreable.length === 0) {\n return {\n unscorable: true,\n reason: 'No criterion carried a numeric score, so no weighted total can be computed.',\n };\n }\n\n const totalWeight = scoreable.reduce((sum, criterion) => sum + (criterion.weight ?? 1), 0);\n if (!(totalWeight > 0)) {\n return {\n unscorable: true,\n reason:\n 'Criterion weights do not sum to a positive number, so the weighted total is undefined.',\n };\n }\n\n const weighted = scoreable.reduce(\n (sum, criterion) => sum + ratioOf(criterion) * (criterion.weight ?? 1),\n 0,\n );\n const rawScore = weighted / totalWeight;\n if (!Number.isFinite(rawScore)) {\n return {\n unscorable: true,\n reason: 'The weighted total is not a finite number, so no grade can be produced.',\n };\n }\n // Clamp the float-noise band only; anything genuinely out of range was\n // already rejected above.\n const score = Math.min(1, Math.max(0, rawScore));\n\n const passed =\n options.passThreshold !== undefined\n ? score >= options.passThreshold\n : activityData !== undefined\n ? computePassThreshold(activityData, score)\n : score >= 0.7;\n\n return {\n score,\n maxScore: 1,\n passed,\n feedback: options.feedback ?? null,\n criteria: [...criteria],\n };\n}\n\n/**\n * Lifts a {@link GradeRecord} into the `graded` arm of {@link ItemOutcome}, so\n * a grade that arrived asynchronously renders through the same path as a\n * synchronously scored item. The score/passed/feedback fields are mirrored\n * onto the outcome for uniform reads; `grade` carries the full record.\n */\nexport function outcomeFromGrade(grade: GradeRecord): ItemOutcome {\n return {\n status: 'graded',\n grade,\n score: grade.score,\n maxScore: grade.maxScore,\n passed: grade.passed,\n feedback: grade.feedback,\n };\n}\n\n/**\n * True when an outcome carries a real grade — either the SDK scored it\n * synchronously or a grader returned one. Use this instead of testing\n * `status === 'scored'`, which silently misses asynchronously graded work.\n */\nexport function hasGrade(\n outcome: ItemOutcome,\n): outcome is Extract<ItemOutcome, { status: 'scored' | 'graded' }> {\n return outcome.status === 'scored' || outcome.status === 'graded';\n}\n","import { hasGrade } from '../grading.js';\nimport type { ItemOutcome } from '../types/activity.js';\nimport { gte, type RoundingPolicy, roundGrade } from './rounding.js';\n\n/**\n * Sectioned assessment scoring — weights, per-section thresholds, and an\n * explicit reason when an attempt fails.\n *\n * This exists because the formula is invariably implemented twice: once on the\n * server that records the grade, and again on the client that shows a learner\n * their grade breakdown. Two implementations of one formula is exactly the\n * duplication an SDK should remove, and they drift — usually in the scale\n * (0–1 vs 0–100) or in whether a section override is honoured.\n */\n\n/** One item's contribution to a section. */\nexport interface ScoredItem {\n /**\n * Identity of the SLOT, not the activity. The same activity can legitimately\n * appear in two sections; keying on the activity id collapses them into one\n * and silently scores the second occurrence as zero.\n */\n slotId: string;\n /** The activity that filled this slot, for reporting. */\n activityId?: string;\n /** Maximum points this slot is worth. */\n points: number;\n /** What the learner achieved, or why there is no grade yet. */\n outcome: ItemOutcome;\n}\n\n/** A weighted section of an assessment. */\nexport interface AssessmentSectionInput {\n id: string;\n title?: string;\n /** Relative weight. Weights are normalised by their sum, so they need not total 1. */\n weight: number;\n /** Overrides the assessment-wide section threshold for this section only. */\n passThresholdOverride?: number;\n items: ScoredItem[];\n}\n\n/** Policy for {@link composeAssessmentScore}. */\nexport interface CompositionPolicy {\n /** Scaled [0,1] overall score required to pass. */\n passThreshold: number;\n /** Scaled [0,1] score each section must reach, when sections gate the pass. */\n sectionThreshold?: number;\n /**\n * How grades are rounded. Required, with no default: see `RoundingPolicy` —\n * grade rounding and band classification are different operations and the\n * SDK must not choose either for you.\n */\n rounding: RoundingPolicy;\n}\n\n/** Per-section result. */\nexport interface SectionScore {\n id: string;\n title?: string;\n /** The authored weight, verbatim. */\n weight: number;\n /**\n * The weight ACTUALLY used in the total, so a client can rebuild the grade\n * from `sections[]` and agree with the record:\n *\n * ```ts\n * roundGrade(\n * sections.reduce((sum, s) => sum + s.score * s.normalizedWeight, 0),\n * policy.rounding,\n * ) === result.score // exact, by construction\n * ```\n *\n * Apply the same final rounding: the raw weighted sum of already-rounded\n * section scores is not itself a rounded value (0.85 and 1.00 at equal\n * weights sum to 0.925 against a recorded 0.93), so comparing it unrounded\n * is off by up to half a quantum.\n *\n * Sections with nothing graded carry `0` here, because they contribute\n * nothing; the remaining weights are renormalised among themselves.\n */\n normalizedWeight: number;\n earnedPoints: number;\n /** Points that are currently gradable — excludes items still awaiting a grade. */\n gradedMaxPoints: number;\n /** Every point in the section, whether graded yet or not. */\n maxPoints: number;\n /** Scaled [0,1] over the GRADED points, rounded once. */\n score: number;\n passed: boolean;\n /** Threshold this section was judged against, after any override. */\n appliedThreshold: number | null;\n /** Slots still awaiting a grade. */\n pendingSlotIds: string[];\n /** Slots that can never be graded, excluded from the denominator. */\n unscorableSlotIds: string[];\n}\n\n/** Why an attempt failed, or `null` when it passed. */\nexport type PassFailureReason =\n | 'overall_below_threshold'\n | 'section_below_threshold'\n | 'both'\n | null;\n\n/** Result of composing an assessment. */\nexport interface AssessmentScore {\n sections: SectionScore[];\n /** Weighted total, scaled [0,1], rounded once. */\n score: number;\n /**\n * Whether the attempt passed — `null` while `status` is `provisional`,\n * because an attempt with work still ungraded has not passed OR failed yet.\n * Returning `false` there would let a UI keyed on `passed` show a fail for\n * an essay nobody has marked.\n */\n passed: boolean | null;\n /** `null` while provisional, for the same reason as {@link passed}. */\n passFailureReason: PassFailureReason;\n /**\n * `provisional` while any item is still awaiting a grade — the total is\n * computed over what HAS been graded, so it can still move. Do not record a\n * provisional score as final. Items that can NEVER be graded\n * (`unscorableSlotIds`) do not hold the result provisional.\n */\n status: 'final' | 'provisional';\n pendingSlotIds: string[];\n /** Slots that can never be graded. Excluded from the denominator. */\n unscorableSlotIds: string[];\n}\n\n/**\n * What one item contributes. THREE states, not two — collapsing the last two\n * into a single \"no points\" answer is what made an unscorable item block an\n * attempt from ever becoming final:\n *\n * - `graded` — real points, counted.\n * - `pending` — a grade is coming (deferred). Excluded from the denominator,\n * and the assessment stays `provisional` until it arrives.\n * - `unscorable` — a grade is never coming (unregistered type, redacted data,\n * incomplete key). Also excluded from the denominator, but it must NOT keep\n * the result provisional forever: `evaluate()` returns this precisely so a\n * mixed-version content bank does not crash an exam, and an attempt that\n * contains one still has to be recordable.\n */\ntype ItemContribution =\n | { state: 'graded'; points: number }\n | { state: 'pending' }\n | { state: 'unscorable' };\n\nfunction earned(item: ScoredItem): ItemContribution {\n const { outcome } = item;\n if (hasGrade(outcome)) {\n const max = outcome.maxScore > 0 ? outcome.maxScore : 1;\n return { state: 'graded', points: (outcome.score / max) * item.points };\n }\n if (outcome.status === 'deferred') {\n return { state: 'pending' };\n }\n return { state: 'unscorable' };\n}\n\n/**\n * Composes per-item outcomes into a sectioned assessment score.\n *\n * Weights are normalised by their sum. Each section's score is computed over\n * the points that are actually gradable and rounded ONCE, before any threshold\n * comparison, so the number a learner is shown is the number that decides the\n * outcome. Items still awaiting a grade are excluded from the denominator\n * rather than counted as zero, and the result is reported as `provisional`\n * until every item has a grade.\n */\nexport function composeAssessmentScore(\n sections: readonly AssessmentSectionInput[],\n policy: CompositionPolicy,\n): AssessmentScore {\n const pendingAll: string[] = [];\n const unscorableAll: string[] = [];\n\n // Pass 1 — score each section over the points that are actually gradable.\n const partials = sections.map((section) => {\n let earnedPoints = 0;\n let gradedMaxPoints = 0;\n let maxPoints = 0;\n const pendingSlotIds: string[] = [];\n const unscorableSlotIds: string[] = [];\n\n for (const item of section.items) {\n maxPoints += item.points;\n const contribution = earned(item);\n if (contribution.state === 'pending') {\n pendingSlotIds.push(item.slotId);\n pendingAll.push(item.slotId);\n continue;\n }\n if (contribution.state === 'unscorable') {\n unscorableSlotIds.push(item.slotId);\n unscorableAll.push(item.slotId);\n continue;\n }\n earnedPoints += contribution.points;\n gradedMaxPoints += item.points;\n }\n\n const raw = gradedMaxPoints > 0 ? earnedPoints / gradedMaxPoints : 0;\n const appliedThreshold = section.passThresholdOverride ?? policy.sectionThreshold ?? null;\n\n return {\n section,\n earnedPoints,\n gradedMaxPoints,\n maxPoints,\n score: roundGrade(raw, policy.rounding),\n appliedThreshold,\n pendingSlotIds,\n unscorableSlotIds,\n };\n });\n\n // Pass 2 — the LIVE weights. Only sections with something graded contribute,\n // and their weights are renormalised among themselves. A section whose items\n // are all still ungraded must not contribute a zero: that is the \"live\n // denominator\" bug, where a midterm reads 50% only because the essay has not\n // been marked and a learner sees a fail for work nobody has looked at.\n // These are the weights reported on each section, so that\n // `sum(score * normalizedWeight) === result.score` holds and a client\n // rendering the breakdown cannot disagree with the recorded grade.\n const contributingWeight = partials.reduce(\n (sum, partial) => sum + (partial.gradedMaxPoints > 0 ? partial.section.weight : 0),\n 0,\n );\n\n const scored: SectionScore[] = partials.map((partial) => ({\n id: partial.section.id,\n ...(partial.section.title !== undefined ? { title: partial.section.title } : {}),\n weight: partial.section.weight,\n normalizedWeight:\n partial.gradedMaxPoints > 0 && contributingWeight > 0\n ? partial.section.weight / contributingWeight\n : 0,\n earnedPoints: partial.earnedPoints,\n gradedMaxPoints: partial.gradedMaxPoints,\n maxPoints: partial.maxPoints,\n score: partial.score,\n // A section with nothing graded yet cannot be said to have failed.\n passed:\n partial.appliedThreshold === null || partial.gradedMaxPoints === 0\n ? true\n : gte(partial.score, partial.appliedThreshold, policy.rounding),\n appliedThreshold: partial.appliedThreshold,\n pendingSlotIds: partial.pendingSlotIds,\n unscorableSlotIds: partial.unscorableSlotIds,\n }));\n\n const weightedRaw = scored.reduce(\n (sum, section) => sum + section.score * section.normalizedWeight,\n 0,\n );\n const score = roundGrade(weightedRaw, policy.rounding);\n\n // Only work that is still COMING keeps the result provisional. Work that can\n // never be graded is excluded from the denominator but must not block the\n // attempt from being recorded.\n const status: 'final' | 'provisional' = pendingAll.length > 0 ? 'provisional' : 'final';\n\n if (status === 'provisional') {\n // Not passed and not failed: undetermined. Saying `false` here would let a\n // UI keyed on `passed` show a fail for unmarked work.\n return {\n sections: scored,\n score,\n passed: null,\n passFailureReason: null,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n }\n\n // Nothing was gradable at all — every item unscorable, or an assessment with\n // no items. There is no evidence either way, so there is no verdict: a hard\n // `false` here records a fail at 0% for a learner whose work was never\n // gradable, which is the same fabricated-failure this module exists to\n // avoid, just reached through the other door.\n if (scored.every((section) => section.gradedMaxPoints === 0)) {\n return {\n sections: scored,\n score,\n passed: null,\n passFailureReason: null,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n }\n\n const overallOk = gte(score, policy.passThreshold, policy.rounding);\n const sectionsOk = scored.every((section) => section.passed);\n const passed = overallOk && sectionsOk;\n\n const passFailureReason: PassFailureReason = passed\n ? null\n : !overallOk && !sectionsOk\n ? 'both'\n : overallOk\n ? 'section_below_threshold'\n : 'overall_below_threshold';\n\n return {\n sections: scored,\n score,\n passed,\n passFailureReason,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n}\n","import { DeferredScoringError, RedactedScoringError, UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport type {\n ActivityData,\n ActivityFeedback,\n ActivityType,\n ItemOutcome,\n LearnerResponse,\n ScoringResult,\n} from '../types/activity.js';\nimport { gte, type RoundingPolicy } from './rounding.js';\n\nexport type {\n AssessmentScore,\n AssessmentSectionInput,\n CompositionPolicy,\n PassFailureReason,\n ScoredItem,\n SectionScore,\n} from './compose.js';\nexport { composeAssessmentScore } from './compose.js';\nexport type { Band, RoundingMode, RoundingPolicy } from './rounding.js';\nexport { classifyBand, gte, roundGrade } from './rounding.js';\nexport type { TextMatchPolicy, TextMatchResult } from './text-match.js';\nexport { levenshteinDistance, matchText } from './text-match.js';\n\n/** Default minimum scaled score required to pass when `passThreshold` is absent. */\nexport const DEFAULT_PASS_THRESHOLD = 0.7;\n\n/**\n * Returns `true` iff `score` meets or exceeds the activity's `passThreshold`,\n * defaulting to {@link DEFAULT_PASS_THRESHOLD} (0.7) when the field is absent.\n *\n * Pass a {@link RoundingPolicy} to compare the way an assessment total is\n * compared — both sides rounded, via {@link gte} — so an item shown as \"70%\"\n * cannot be recorded as a fail at 69.6. It is **opt-in** rather than the\n * default because switching it on changes item-level pass/fail for scores in\n * the rounding band, and this SDK does not alter historical grades without an\n * explicit decision. Absent, the comparison is the exact raw `>=` it has\n * always been.\n */\nexport function computePassThreshold(\n activityData: ActivityData,\n score: number,\n rounding?: RoundingPolicy,\n): boolean {\n const threshold = activityData.passThreshold ?? DEFAULT_PASS_THRESHOLD;\n return rounding === undefined ? score >= threshold : gte(score, threshold, rounding);\n}\n\n/**\n * True when `data` is a `redact()` projection rather than full activity data.\n * Scoring a redacted item is always a bug: the answer key is gone by design,\n * so any \"score\" computed from it is meaningless (it used to come out `NaN`).\n */\nfunction isRedacted(data: unknown): boolean {\n return (\n typeof data === 'object' && data !== null && (data as { redacted?: unknown }).redacted === true\n );\n}\n\n/**\n * Selects the authored overall feedback for a result: `feedback.correct` when\n * the learner passed, `feedback.incorrect` otherwise; `null` when no matching\n * message was authored. Mirrors the selection the lk-react components applied\n * (keyed on `passed`, per the `ActivityFeedback` contract).\n */\nfunction selectFeedback(activityData: ActivityData, passed: boolean): string | null {\n const feedback = (activityData as { feedback?: ActivityFeedback }).feedback;\n if (feedback === undefined) {\n return null;\n }\n return (passed ? feedback.correct : feedback.incorrect) ?? null;\n}\n\n/**\n * Scores a learner response against activity data and returns a full\n * {@link ScoringResult}.\n *\n * Pure and deterministic with no side effects. It does **not** re-validate\n * `activityData` — schema validation is the component boundary's\n * responsibility; this is a low-level scoring primitive that trusts its typed\n * inputs. Dispatch is registry-backed: consumer-registered types with `sync`\n * scoring work here too. An unregistered type throws\n * {@link UnknownActivityTypeError}; a type whose grading is deferred (e.g.\n * `written-response`) throws {@link DeferredScoringError} — use\n * {@link evaluate}, which can express \"not graded yet\".\n */\nexport function score(\n activityType: ActivityType,\n activityData: ActivityData,\n learnerResponse: LearnerResponse,\n): ScoringResult {\n const descriptor = getActivityTypeDescriptor(activityType);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(activityType));\n }\n if (descriptor.scoring.kind === 'deferred') {\n throw new DeferredScoringError(descriptor.type);\n }\n if (isRedacted(activityData)) {\n throw new RedactedScoringError(descriptor.type);\n }\n\n const result = descriptor.scoring.score(activityData, learnerResponse);\n if (!Number.isFinite(result.score)) {\n throw new RedactedScoringError(descriptor.type);\n }\n const passed = computePassThreshold(activityData, result.score);\n return { ...result, passed, feedback: result.feedback ?? selectFeedback(activityData, passed) };\n}\n\n/**\n * Evaluates a learner response against an activity and returns an\n * {@link ItemOutcome} — the resilient, forward-compatible alternative to\n * {@link score}:\n *\n * - Synchronously graded types return `{ status: 'scored', ... }` with the\n * same numbers `score()` produces.\n * - Asynchronously graded types (e.g. `written-response`) return\n * `{ status: 'deferred', reason, partial }` instead of a fake zero — \"not\n * graded yet\" is expressible in the type system, never conflated with\n * \"wrong\".\n * - An unregistered `data.type` returns `{ status: 'unscorable' }` rather\n * than throwing, so a mixed-version content bank cannot crash an exam run.\n *\n * The activity type is read from `data.type` — there is no separate type\n * parameter to disagree with the payload.\n */\nexport function evaluate(data: ActivityData, response: LearnerResponse): ItemOutcome {\n const type = (data as { type?: unknown }).type;\n const descriptor = typeof type === 'string' ? getActivityTypeDescriptor(type) : undefined;\n\n if (descriptor === undefined) {\n return {\n status: 'unscorable',\n reason: `Activity type \"${String(type)}\" is not registered`,\n maxScore: 1,\n };\n }\n\n if (descriptor.scoring.kind === 'deferred') {\n const partial = descriptor.scoring.partial?.(data, response);\n return {\n status: 'deferred',\n reason: descriptor.scoring.reason,\n maxScore: 1,\n ...(partial !== undefined ? { partial } : {}),\n };\n }\n\n if (isRedacted(data)) {\n return {\n status: 'unscorable',\n reason:\n 'Activity data is redacted (no answer key), so it cannot be scored on the client. Score against the full data server-side.',\n maxScore: 1,\n };\n }\n\n const result = descriptor.scoring.score(data, response);\n if (!Number.isFinite(result.score)) {\n // Defence in depth: incomplete data (a missing answer key, an empty\n // options array) used to divide by zero and surface as a real score of\n // NaN, which JSON-serializes to null in a grade column.\n return {\n status: 'unscorable',\n reason: `Scoring \"${descriptor.type}\" produced a non-finite score; the activity data is incomplete.`,\n maxScore: result.maxScore,\n };\n }\n const passed = computePassThreshold(data, result.score);\n return {\n status: 'scored',\n score: result.score,\n maxScore: result.maxScore,\n passed,\n feedback: result.feedback ?? selectFeedback(data, passed),\n details: result.details,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-4VH7RUX6.cjs","../src/scoring/rounding.ts","../src/grading.ts","../src/scoring/compose.ts","../src/scoring/index.ts"],"names":["EPSILON","score"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-4VH7RUX6.cjs","sourcesContent":[null,"/**\n * Rounding for assessment scores.\n *\n * Rounding is **two different operations** that must not share one policy, and\n * conflating them silently inverts one of them:\n *\n * - **Grade rounding** decides the number a learner is shown and recorded\n * against. It rounds to a fixed number of decimal places, conventionally\n * half-up, so a learner shown \"70%\" is not recorded as a fail at 69.6.\n * - **Band / level classification** decides which level someone is placed in.\n * It deliberately **floors**: placing a learner above their real level is\n * the more harmful error, so a band boundary must not be reached by\n * rounding up.\n *\n * There is therefore no default `RoundingPolicy` anywhere in this SDK, and\n * `dp` has no default either — an integrator's deliberate choice must never be\n * supplied by us.\n */\n\n/** How a value is rounded to `dp` decimal places. */\nexport type RoundingMode = 'half-up' | 'half-even' | 'floor' | 'ceil';\n\n/** A rounding policy. Both fields are required — the SDK never guesses either. */\nexport interface RoundingPolicy {\n mode: RoundingMode;\n /** Decimal places. Load-bearing: `dp: 2` is what stops 69.6 becoming a fail at 70. */\n dp: number;\n}\n\n/**\n * Float-noise guard. Binary floating point makes `1.005 * 100` come out as\n * `100.49999999999999` and `0.29 * 100` come out as `28.999999999999996`;\n * without an allowance both would move a learner's grade a step. It is applied\n * in the direction each mode needs — see {@link roundGrade}.\n */\nconst EPSILON = 1e-9;\n\n/**\n * Tolerance for deciding \"is this exactly halfway?\" in half-even mode.\n * Deliberately much larger than {@link EPSILON}: if the tie test shared the\n * nudge's tolerance, the nudge would decide its own outcome. Safe for grades,\n * which live on a [0,1] scale where representation error is far below this.\n */\nconst TIE_TOLERANCE = 1e-6;\n\nfunction scaled(value: number, dp: number): number {\n return value * 10 ** dp;\n}\n\n/**\n * Normalises `-0` to `0` without touching anything else. `-0` JSON-serialises\n * as `0` but fails `Object.is(-0, 0)`, which surprises a strict-equality\n * gradebook comparison. Deliberately NOT `|| 0`: that also swallowed `NaN`\n * (reachable with an absurd `dp`, where `10 ** dp` overflows to Infinity) and\n * turned a visible failure into a plausible-looking grade of zero.\n */\nfunction noNegZero(value: number): number {\n return Object.is(value, -0) ? 0 : value;\n}\n\n/**\n * Rounds `value` to `policy.dp` places under `policy.mode`.\n *\n * Idempotent for every mode: rounding an already-rounded value returns it\n * unchanged. That matters because a composed score is rounded once and then\n * compared through {@link gte}, which rounds again.\n */\nexport function roundGrade(value: number, policy: RoundingPolicy): number {\n if (!Number.isFinite(value)) {\n return value;\n }\n const factor = 10 ** policy.dp;\n const raw = scaled(value, policy.dp);\n\n // The nudge must oppose the mode, never point uniformly away from zero.\n // `Math.ceil` on a value that is ALREADY exact at `dp` jumps a whole\n // quantum if you add epsilon first: 0.7 became 0.71, a true 0 became 0.01,\n // and the operation stopped being idempotent (0.7 -> 0.71 -> 0.72). So\n // `ceil` nudges down and `floor` nudges up — just far enough to absorb\n // representation error (0.29 * 100 is 28.999999999999996, which must still\n // floor to 29) without ever moving a value that was already exact.\n // Half modes keep the sign-aware nudge, which is what makes a .5 tie land\n // away from zero.\n switch (policy.mode) {\n case 'floor':\n return noNegZero(Math.floor(raw + EPSILON) / factor);\n case 'ceil':\n return noNegZero(Math.ceil(raw - EPSILON) / factor);\n case 'half-even': {\n // Detect the tie on the RAW value, and with a tolerance decoupled from\n // (and far larger than) the nudge epsilon. Testing the NUDGED value\n // against EPSILON let the nudge itself decide whether a tie was seen:\n // 0.375 was detected but 0.125 was not, so half-even silently degraded\n // to half-up for some exact ties — and only ever in the direction that\n // rounds a learner UP, the opposite of what this mode is chosen for.\n const floored = Math.floor(raw);\n if (Math.abs(raw - floored - 0.5) > TIE_TOLERANCE) {\n return noNegZero(halfAway(raw) / factor);\n }\n // Exactly halfway: pick the even neighbour (banker's rounding).\n const even = floored % 2 === 0 ? floored : floored + 1;\n return noNegZero(even / factor);\n }\n default:\n return noNegZero(halfAway(raw) / factor);\n }\n}\n\n/**\n * Half-up on a scaled value, with the sign-aware float-noise nudge.\n * `Math.round` breaks .5 ties toward +Infinity, so negatives are mirrored to\n * keep the tie breaking away from zero in both directions.\n */\nfunction halfAway(raw: number): number {\n const nudged = raw >= 0 ? raw + EPSILON : raw - EPSILON;\n return raw >= 0 ? Math.round(nudged) : -Math.round(-nudged);\n}\n\n/**\n * Threshold comparison that rounds **both sides** before comparing.\n *\n * Comparing a raw float against a rounded threshold is how a learner ends up\n * shown one number and recorded against another. Rounding both sides — and\n * allowing an epsilon — makes \"what the learner sees\" and \"what the gradebook\n * decides\" the same comparison.\n */\nexport function gte(value: number, threshold: number, policy: RoundingPolicy): boolean {\n return roundGrade(value, policy) >= roundGrade(threshold, policy) - EPSILON;\n}\n\n/** A named band with an inclusive lower bound, e.g. `{ name: 'B1', min: 0.6 }`. */\nexport interface Band {\n name: string;\n /** Inclusive lower bound on the same scale as the value being classified. */\n min: number;\n}\n\n/**\n * Classifies a value into a band by **flooring**: the highest band whose `min`\n * the value actually reaches. Deliberately does NOT round up to a boundary —\n * over-placement is the more harmful error, so a learner just below a boundary\n * stays below it.\n *\n * Returns `null` when the value reaches no band's minimum.\n */\nexport function classifyBand(value: number, bands: readonly Band[]): Band | null {\n let best: Band | null = null;\n for (const band of bands) {\n if (value >= band.min - EPSILON && (best === null || band.min > best.min)) {\n best = band;\n }\n }\n return best;\n}\n","import { computePassThreshold } from './scoring/index.js';\nimport type { ActivityData, ItemOutcome } from './types/activity.js';\nimport type { CriterionScore, GradeRecord } from './types/grading.js';\n\nexport type {\n CriterionScore,\n GradeRecord,\n Grader,\n GraderKind,\n GraderUsage,\n GradingState,\n InlineCorrection,\n} from './types/grading.js';\n\n/** Options for {@link gradeFromRubric}. */\nexport interface GradeFromRubricOptions {\n /**\n * Scaled score [0,1] required to pass. Defaults to the activity's\n * `passThreshold`, or {@link DEFAULT_PASS_THRESHOLD} when absent.\n */\n passThreshold?: number;\n /** Narrative feedback for the learner, carried onto the record verbatim. */\n feedback?: string | null;\n}\n\n/**\n * Computes the weighted total from per-criterion scores — the arithmetic that\n * must NOT be delegated to a language model.\n *\n * A grader is asked for judgement (how good is this criterion?), not for\n * mental arithmetic. Asking a model to also produce the weighted total makes\n * the final grade unverifiable and unreproducible: two runs can return\n * different totals for identical criterion scores. This function makes the\n * total a pure function of the judgements, so a grade can be recomputed and\n * audited years later.\n *\n * Weights are normalised by their sum, so they need not add to 1. Criteria\n * marked `notApplicable`, and those carrying no numeric `score` (a purely\n * banded judgement), are excluded from both numerator and denominator. When\n * nothing scoreable remains the result is `unscorable`, never a zero.\n *\n * Scores need not be in [0,1]: set `maxScore` on a criterion to declare what\n * its score is out of, and each is normalised before weighting. A grader\n * working out of 100 says so and is done:\n *\n * ```ts\n * gradeFromRubric([\n * { name: 'Task achievement', score: 82, maxScore: 100, weight: 2 },\n * { name: 'Range', score: 7, maxScore: 9, weight: 1 },\n * ]);\n * ```\n *\n * The returned `GradeRecord.score` is always scaled [0,1] against\n * `maxScore: 1`, like every other score in the SDK.\n */\nexport function gradeFromRubric(\n criteria: readonly CriterionScore[],\n activityData?: ActivityData,\n options: GradeFromRubricOptions = {},\n): GradeRecord | { unscorable: true; reason: string } {\n // A corrupt score is an ERROR, not an exclusion. Exclusions (`notApplicable`,\n // and band-only criteria with no numeric score) are deliberate authoring\n // decisions; a NaN or Infinity is a broken grader. Silently dropping it\n // would regrade the learner on fewer criteria — with different effective\n // weights — and nobody would know.\n const corrupt = criteria.find(\n (criterion) =>\n criterion.notApplicable !== true &&\n criterion.score !== undefined &&\n !Number.isFinite(criterion.score),\n );\n if (corrupt !== undefined) {\n return {\n unscorable: true,\n reason: `Criterion \"${corrupt.name}\" has a non-finite score (${String(corrupt.score)}). A grade cannot be computed from it.`,\n };\n }\n\n // A `maxScore` that is absent means 1 (the scaled convention). One that is\n // present must be a positive finite number: dividing by 0, a negative, or a\n // NaN would produce a grade nobody can defend.\n const badMax = criteria.find(\n (criterion) =>\n criterion.notApplicable !== true &&\n criterion.maxScore !== undefined &&\n !(Number.isFinite(criterion.maxScore) && criterion.maxScore > 0),\n );\n if (badMax !== undefined) {\n return {\n unscorable: true,\n reason: `Criterion \"${badMax.name}\" declares maxScore ${String(badMax.maxScore)}; it must be a positive, finite number.`,\n };\n }\n\n const scoreable = criteria.filter(\n (criterion) => criterion.notApplicable !== true && typeof criterion.score === 'number',\n );\n\n /** The criterion's score as a ratio in [0,1]. `maxScore` defaults to 1. */\n const ratioOf = (criterion: CriterionScore): number =>\n (criterion.score as number) / (criterion.maxScore ?? 1);\n\n // A grader that returns raw points (4 out of 5) without declaring\n // `maxScore: 5` would otherwise yield `{ score: 4, maxScore: 1 }` and pass\n // every threshold. Reject rather than clamp: silently rescaling someone's\n // grader is worse than telling them it is out of contract. A hair outside\n // the range is float noise and is clamped further down.\n const EPSILON = 1e-9;\n const outOfRange = scoreable.find(\n (criterion) => ratioOf(criterion) < -EPSILON || ratioOf(criterion) > 1 + EPSILON,\n );\n if (outOfRange !== undefined) {\n return {\n unscorable: true,\n reason:\n `Criterion \"${outOfRange.name}\" has score ${outOfRange.score} out of ${outOfRange.maxScore ?? 1}, ` +\n 'which is outside the [0,1] range once scaled. Set `maxScore` on the criterion to declare what the ' +\n 'score is out of (e.g. `maxScore: 100` for a 0–100 grader).',\n };\n }\n\n if (scoreable.length === 0) {\n return {\n unscorable: true,\n reason: 'No criterion carried a numeric score, so no weighted total can be computed.',\n };\n }\n\n const totalWeight = scoreable.reduce((sum, criterion) => sum + (criterion.weight ?? 1), 0);\n if (!(totalWeight > 0)) {\n return {\n unscorable: true,\n reason:\n 'Criterion weights do not sum to a positive number, so the weighted total is undefined.',\n };\n }\n\n const weighted = scoreable.reduce(\n (sum, criterion) => sum + ratioOf(criterion) * (criterion.weight ?? 1),\n 0,\n );\n const rawScore = weighted / totalWeight;\n if (!Number.isFinite(rawScore)) {\n return {\n unscorable: true,\n reason: 'The weighted total is not a finite number, so no grade can be produced.',\n };\n }\n // Clamp the float-noise band only; anything genuinely out of range was\n // already rejected above.\n const score = Math.min(1, Math.max(0, rawScore));\n\n const passed =\n options.passThreshold !== undefined\n ? score >= options.passThreshold\n : activityData !== undefined\n ? computePassThreshold(activityData, score)\n : score >= 0.7;\n\n return {\n score,\n maxScore: 1,\n passed,\n feedback: options.feedback ?? null,\n criteria: [...criteria],\n };\n}\n\n/**\n * Lifts a {@link GradeRecord} into the `graded` arm of {@link ItemOutcome}, so\n * a grade that arrived asynchronously renders through the same path as a\n * synchronously scored item. The score/passed/feedback fields are mirrored\n * onto the outcome for uniform reads; `grade` carries the full record.\n */\nexport function outcomeFromGrade(grade: GradeRecord): ItemOutcome {\n return {\n status: 'graded',\n grade,\n score: grade.score,\n maxScore: grade.maxScore,\n passed: grade.passed,\n feedback: grade.feedback,\n };\n}\n\n/**\n * True when an outcome carries a real grade — either the SDK scored it\n * synchronously or a grader returned one. Use this instead of testing\n * `status === 'scored'`, which silently misses asynchronously graded work.\n */\nexport function hasGrade(\n outcome: ItemOutcome,\n): outcome is Extract<ItemOutcome, { status: 'scored' | 'graded' }> {\n return outcome.status === 'scored' || outcome.status === 'graded';\n}\n","import { hasGrade } from '../grading.js';\nimport type { ItemOutcome } from '../types/activity.js';\nimport { gte, type RoundingPolicy, roundGrade } from './rounding.js';\n\n/**\n * Sectioned assessment scoring — weights, per-section thresholds, and an\n * explicit reason when an attempt fails.\n *\n * This exists because the formula is invariably implemented twice: once on the\n * server that records the grade, and again on the client that shows a learner\n * their grade breakdown. Two implementations of one formula is exactly the\n * duplication an SDK should remove, and they drift — usually in the scale\n * (0–1 vs 0–100) or in whether a section override is honoured.\n */\n\n/** One item's contribution to a section. */\nexport interface ScoredItem {\n /**\n * Identity of the SLOT, not the activity. The same activity can legitimately\n * appear in two sections; keying on the activity id collapses them into one\n * and silently scores the second occurrence as zero.\n */\n slotId: string;\n /** The activity that filled this slot, for reporting. */\n activityId?: string;\n /** Maximum points this slot is worth. */\n points: number;\n /** What the learner achieved, or why there is no grade yet. */\n outcome: ItemOutcome;\n}\n\n/** A weighted section of an assessment. */\nexport interface AssessmentSectionInput {\n id: string;\n title?: string;\n /** Relative weight. Weights are normalised by their sum, so they need not total 1. */\n weight: number;\n /** Overrides the assessment-wide section threshold for this section only. */\n passThresholdOverride?: number;\n items: ScoredItem[];\n}\n\n/** Policy for {@link composeAssessmentScore}. */\nexport interface CompositionPolicy {\n /** Scaled [0,1] overall score required to pass. */\n passThreshold: number;\n /** Scaled [0,1] score each section must reach, when sections gate the pass. */\n sectionThreshold?: number;\n /**\n * How grades are rounded. Required, with no default: see `RoundingPolicy` —\n * grade rounding and band classification are different operations and the\n * SDK must not choose either for you.\n */\n rounding: RoundingPolicy;\n}\n\n/** Per-section result. */\nexport interface SectionScore {\n id: string;\n title?: string;\n /** The authored weight, verbatim. */\n weight: number;\n /**\n * The weight ACTUALLY used in the total, so a client can rebuild the grade\n * from `sections[]` and agree with the record:\n *\n * ```ts\n * roundGrade(\n * sections.reduce((sum, s) => sum + s.score * s.normalizedWeight, 0),\n * policy.rounding,\n * ) === result.score // exact, by construction\n * ```\n *\n * Apply the same final rounding: the raw weighted sum of already-rounded\n * section scores is not itself a rounded value (0.85 and 1.00 at equal\n * weights sum to 0.925 against a recorded 0.93), so comparing it unrounded\n * is off by up to half a quantum.\n *\n * Sections with nothing graded carry `0` here, because they contribute\n * nothing; the remaining weights are renormalised among themselves.\n */\n normalizedWeight: number;\n earnedPoints: number;\n /** Points that are currently gradable — excludes items still awaiting a grade. */\n gradedMaxPoints: number;\n /** Every point in the section, whether graded yet or not. */\n maxPoints: number;\n /** Scaled [0,1] over the GRADED points, rounded once. */\n score: number;\n passed: boolean;\n /** Threshold this section was judged against, after any override. */\n appliedThreshold: number | null;\n /** Slots still awaiting a grade. */\n pendingSlotIds: string[];\n /** Slots that can never be graded, excluded from the denominator. */\n unscorableSlotIds: string[];\n}\n\n/** Why an attempt failed, or `null` when it passed. */\nexport type PassFailureReason =\n | 'overall_below_threshold'\n | 'section_below_threshold'\n | 'both'\n | null;\n\n/** Result of composing an assessment. */\nexport interface AssessmentScore {\n sections: SectionScore[];\n /** Weighted total, scaled [0,1], rounded once. */\n score: number;\n /**\n * Whether the attempt passed — `null` while `status` is `provisional`,\n * because an attempt with work still ungraded has not passed OR failed yet.\n * Returning `false` there would let a UI keyed on `passed` show a fail for\n * an essay nobody has marked.\n */\n passed: boolean | null;\n /** `null` while provisional, for the same reason as {@link passed}. */\n passFailureReason: PassFailureReason;\n /**\n * `provisional` while any item is still awaiting a grade — the total is\n * computed over what HAS been graded, so it can still move. Do not record a\n * provisional score as final. Items that can NEVER be graded\n * (`unscorableSlotIds`) do not hold the result provisional.\n */\n status: 'final' | 'provisional';\n pendingSlotIds: string[];\n /** Slots that can never be graded. Excluded from the denominator. */\n unscorableSlotIds: string[];\n}\n\n/**\n * What one item contributes. THREE states, not two — collapsing the last two\n * into a single \"no points\" answer is what made an unscorable item block an\n * attempt from ever becoming final:\n *\n * - `graded` — real points, counted.\n * - `pending` — a grade is coming (deferred). Excluded from the denominator,\n * and the assessment stays `provisional` until it arrives.\n * - `unscorable` — a grade is never coming (unregistered type, redacted data,\n * incomplete key). Also excluded from the denominator, but it must NOT keep\n * the result provisional forever: `evaluate()` returns this precisely so a\n * mixed-version content bank does not crash an exam, and an attempt that\n * contains one still has to be recordable.\n */\ntype ItemContribution =\n | { state: 'graded'; points: number }\n | { state: 'pending' }\n | { state: 'unscorable' };\n\nfunction earned(item: ScoredItem): ItemContribution {\n const { outcome } = item;\n if (hasGrade(outcome)) {\n const max = outcome.maxScore > 0 ? outcome.maxScore : 1;\n return { state: 'graded', points: (outcome.score / max) * item.points };\n }\n if (outcome.status === 'deferred') {\n return { state: 'pending' };\n }\n return { state: 'unscorable' };\n}\n\n/**\n * Composes per-item outcomes into a sectioned assessment score.\n *\n * Weights are normalised by their sum. Each section's score is computed over\n * the points that are actually gradable and rounded ONCE, before any threshold\n * comparison, so the number a learner is shown is the number that decides the\n * outcome. Items still awaiting a grade are excluded from the denominator\n * rather than counted as zero, and the result is reported as `provisional`\n * until every item has a grade.\n */\nexport function composeAssessmentScore(\n sections: readonly AssessmentSectionInput[],\n policy: CompositionPolicy,\n): AssessmentScore {\n const pendingAll: string[] = [];\n const unscorableAll: string[] = [];\n\n // Pass 1 — score each section over the points that are actually gradable.\n const partials = sections.map((section) => {\n let earnedPoints = 0;\n let gradedMaxPoints = 0;\n let maxPoints = 0;\n const pendingSlotIds: string[] = [];\n const unscorableSlotIds: string[] = [];\n\n for (const item of section.items) {\n maxPoints += item.points;\n const contribution = earned(item);\n if (contribution.state === 'pending') {\n pendingSlotIds.push(item.slotId);\n pendingAll.push(item.slotId);\n continue;\n }\n if (contribution.state === 'unscorable') {\n unscorableSlotIds.push(item.slotId);\n unscorableAll.push(item.slotId);\n continue;\n }\n earnedPoints += contribution.points;\n gradedMaxPoints += item.points;\n }\n\n const raw = gradedMaxPoints > 0 ? earnedPoints / gradedMaxPoints : 0;\n const appliedThreshold = section.passThresholdOverride ?? policy.sectionThreshold ?? null;\n\n return {\n section,\n earnedPoints,\n gradedMaxPoints,\n maxPoints,\n score: roundGrade(raw, policy.rounding),\n appliedThreshold,\n pendingSlotIds,\n unscorableSlotIds,\n };\n });\n\n // Pass 2 — the LIVE weights. Only sections with something graded contribute,\n // and their weights are renormalised among themselves. A section whose items\n // are all still ungraded must not contribute a zero: that is the \"live\n // denominator\" bug, where a midterm reads 50% only because the essay has not\n // been marked and a learner sees a fail for work nobody has looked at.\n // These are the weights reported on each section, so that\n // `sum(score * normalizedWeight) === result.score` holds and a client\n // rendering the breakdown cannot disagree with the recorded grade.\n const contributingWeight = partials.reduce(\n (sum, partial) => sum + (partial.gradedMaxPoints > 0 ? partial.section.weight : 0),\n 0,\n );\n\n const scored: SectionScore[] = partials.map((partial) => ({\n id: partial.section.id,\n ...(partial.section.title !== undefined ? { title: partial.section.title } : {}),\n weight: partial.section.weight,\n normalizedWeight:\n partial.gradedMaxPoints > 0 && contributingWeight > 0\n ? partial.section.weight / contributingWeight\n : 0,\n earnedPoints: partial.earnedPoints,\n gradedMaxPoints: partial.gradedMaxPoints,\n maxPoints: partial.maxPoints,\n score: partial.score,\n // A section with nothing graded yet cannot be said to have failed.\n passed:\n partial.appliedThreshold === null || partial.gradedMaxPoints === 0\n ? true\n : gte(partial.score, partial.appliedThreshold, policy.rounding),\n appliedThreshold: partial.appliedThreshold,\n pendingSlotIds: partial.pendingSlotIds,\n unscorableSlotIds: partial.unscorableSlotIds,\n }));\n\n const weightedRaw = scored.reduce(\n (sum, section) => sum + section.score * section.normalizedWeight,\n 0,\n );\n const score = roundGrade(weightedRaw, policy.rounding);\n\n // Only work that is still COMING keeps the result provisional. Work that can\n // never be graded is excluded from the denominator but must not block the\n // attempt from being recorded.\n const status: 'final' | 'provisional' = pendingAll.length > 0 ? 'provisional' : 'final';\n\n if (status === 'provisional') {\n // Not passed and not failed: undetermined. Saying `false` here would let a\n // UI keyed on `passed` show a fail for unmarked work.\n return {\n sections: scored,\n score,\n passed: null,\n passFailureReason: null,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n }\n\n // Nothing was gradable at all — every item unscorable, or an assessment with\n // no items. There is no evidence either way, so there is no verdict: a hard\n // `false` here records a fail at 0% for a learner whose work was never\n // gradable, which is the same fabricated-failure this module exists to\n // avoid, just reached through the other door.\n if (scored.every((section) => section.gradedMaxPoints === 0)) {\n return {\n sections: scored,\n score,\n passed: null,\n passFailureReason: null,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n }\n\n const overallOk = gte(score, policy.passThreshold, policy.rounding);\n const sectionsOk = scored.every((section) => section.passed);\n const passed = overallOk && sectionsOk;\n\n const passFailureReason: PassFailureReason = passed\n ? null\n : !overallOk && !sectionsOk\n ? 'both'\n : overallOk\n ? 'section_below_threshold'\n : 'overall_below_threshold';\n\n return {\n sections: scored,\n score,\n passed,\n passFailureReason,\n status,\n pendingSlotIds: pendingAll,\n unscorableSlotIds: unscorableAll,\n };\n}\n","import { DeferredScoringError, RedactedScoringError, UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport type {\n ActivityData,\n ActivityFeedback,\n ActivityType,\n ItemOutcome,\n LearnerResponse,\n ScoringResult,\n} from '../types/activity.js';\nimport { gte, type RoundingPolicy } from './rounding.js';\n\nexport type {\n AssessmentScore,\n AssessmentSectionInput,\n CompositionPolicy,\n PassFailureReason,\n ScoredItem,\n SectionScore,\n} from './compose.js';\nexport { composeAssessmentScore } from './compose.js';\nexport type { Band, RoundingMode, RoundingPolicy } from './rounding.js';\nexport { classifyBand, gte, roundGrade } from './rounding.js';\nexport type { TextMatchPolicy, TextMatchResult } from './text-match.js';\nexport { levenshteinDistance, matchText } from './text-match.js';\n\n/** Default minimum scaled score required to pass when `passThreshold` is absent. */\nexport const DEFAULT_PASS_THRESHOLD = 0.7;\n\n/**\n * Returns `true` iff `score` meets or exceeds the activity's `passThreshold`,\n * defaulting to {@link DEFAULT_PASS_THRESHOLD} (0.7) when the field is absent.\n *\n * Pass a {@link RoundingPolicy} to compare the way an assessment total is\n * compared — both sides rounded, via {@link gte} — so an item shown as \"70%\"\n * cannot be recorded as a fail at 69.6. It is **opt-in** rather than the\n * default because switching it on changes item-level pass/fail for scores in\n * the rounding band, and this SDK does not alter historical grades without an\n * explicit decision. Absent, the comparison is the exact raw `>=` it has\n * always been.\n */\nexport function computePassThreshold(\n activityData: ActivityData,\n score: number,\n rounding?: RoundingPolicy,\n): boolean {\n const threshold = activityData.passThreshold ?? DEFAULT_PASS_THRESHOLD;\n return rounding === undefined ? score >= threshold : gte(score, threshold, rounding);\n}\n\n/**\n * True when `data` is a `redact()` projection rather than full activity data.\n * Scoring a redacted item is always a bug: the answer key is gone by design,\n * so any \"score\" computed from it is meaningless (it used to come out `NaN`).\n */\nfunction isRedacted(data: unknown): boolean {\n return (\n typeof data === 'object' && data !== null && (data as { redacted?: unknown }).redacted === true\n );\n}\n\n/**\n * Selects the authored overall feedback for a result: `feedback.correct` when\n * the learner passed, `feedback.incorrect` otherwise; `null` when no matching\n * message was authored. Mirrors the selection the lk-react components applied\n * (keyed on `passed`, per the `ActivityFeedback` contract).\n */\nfunction selectFeedback(activityData: ActivityData, passed: boolean): string | null {\n const feedback = (activityData as { feedback?: ActivityFeedback }).feedback;\n if (feedback === undefined) {\n return null;\n }\n return (passed ? feedback.correct : feedback.incorrect) ?? null;\n}\n\n/**\n * Scores a learner response against activity data and returns a full\n * {@link ScoringResult}.\n *\n * Pure and deterministic with no side effects. It does **not** re-validate\n * `activityData` — schema validation is the component boundary's\n * responsibility; this is a low-level scoring primitive that trusts its typed\n * inputs. Dispatch is registry-backed: consumer-registered types with `sync`\n * scoring work here too. An unregistered type throws\n * {@link UnknownActivityTypeError}; a type whose grading is deferred (e.g.\n * `written-response`) throws {@link DeferredScoringError} — use\n * {@link evaluate}, which can express \"not graded yet\".\n */\nexport function score(\n activityType: ActivityType,\n activityData: ActivityData,\n learnerResponse: LearnerResponse,\n): ScoringResult {\n const descriptor = getActivityTypeDescriptor(activityType);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(activityType));\n }\n if (descriptor.scoring.kind === 'deferred') {\n throw new DeferredScoringError(descriptor.type);\n }\n if (isRedacted(activityData)) {\n throw new RedactedScoringError(descriptor.type);\n }\n\n const result = descriptor.scoring.score(activityData, learnerResponse);\n if (!Number.isFinite(result.score)) {\n throw new RedactedScoringError(descriptor.type);\n }\n const passed = computePassThreshold(activityData, result.score);\n return { ...result, passed, feedback: result.feedback ?? selectFeedback(activityData, passed) };\n}\n\n/**\n * Evaluates a learner response against an activity and returns an\n * {@link ItemOutcome} — the resilient, forward-compatible alternative to\n * {@link score}:\n *\n * - Synchronously graded types return `{ status: 'scored', ... }` with the\n * same numbers `score()` produces.\n * - Asynchronously graded types (e.g. `written-response`) return\n * `{ status: 'deferred', reason, partial }` instead of a fake zero — \"not\n * graded yet\" is expressible in the type system, never conflated with\n * \"wrong\".\n * - An unregistered `data.type` returns `{ status: 'unscorable' }` rather\n * than throwing, so a mixed-version content bank cannot crash an exam run.\n *\n * The activity type is read from `data.type` — there is no separate type\n * parameter to disagree with the payload.\n */\nexport function evaluate(data: ActivityData, response: LearnerResponse): ItemOutcome {\n const type = (data as { type?: unknown }).type;\n const descriptor = typeof type === 'string' ? getActivityTypeDescriptor(type) : undefined;\n\n if (descriptor === undefined) {\n return {\n status: 'unscorable',\n reason: `Activity type \"${String(type)}\" is not registered`,\n maxScore: 1,\n };\n }\n\n if (descriptor.scoring.kind === 'deferred') {\n const partial = descriptor.scoring.partial?.(data, response);\n return {\n status: 'deferred',\n reason: descriptor.scoring.reason,\n maxScore: 1,\n ...(partial !== undefined ? { partial } : {}),\n };\n }\n\n if (isRedacted(data)) {\n return {\n status: 'unscorable',\n reason:\n 'Activity data is redacted (no answer key), so it cannot be scored on the client. Score against the full data server-side.',\n maxScore: 1,\n };\n }\n\n const result = descriptor.scoring.score(data, response);\n if (!Number.isFinite(result.score)) {\n // Defence in depth: incomplete data (a missing answer key, an empty\n // options array) used to divide by zero and surface as a real score of\n // NaN, which JSON-serializes to null in a grade column.\n return {\n status: 'unscorable',\n reason: `Scoring \"${descriptor.type}\" produced a non-finite score; the activity data is incomplete.`,\n maxScore: result.maxScore,\n };\n }\n const passed = computePassThreshold(data, result.score);\n return {\n status: 'scored',\n score: result.score,\n maxScore: result.maxScore,\n passed,\n feedback: result.feedback ?? selectFeedback(data, passed),\n details: result.details,\n };\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkRHJ5VNFBcjs = require('./chunk-RHJ5VNFB.cjs');
|
|
5
5
|
|
|
6
6
|
// src/xapi/validators.ts
|
|
7
7
|
var _v4 = require('zod/v4');
|
|
@@ -83,7 +83,7 @@ function validateXAPIStatement(statement) {
|
|
|
83
83
|
console.warn("[learning-kit] Invalid xAPI statement (sent anyway):", errors);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
-
throw new (0,
|
|
86
|
+
throw new (0, _chunkRHJ5VNFBcjs.ActivitySchemaError)("xAPIStatement", errors);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/xapi/verbs.ts
|
|
@@ -246,7 +246,7 @@ var xAPIBuilder = {
|
|
|
246
246
|
}
|
|
247
247
|
};
|
|
248
248
|
function xapiDefinitionFor(data) {
|
|
249
|
-
const descriptor =
|
|
249
|
+
const descriptor = _chunkRHJ5VNFBcjs.getActivityTypeDescriptor.call(void 0, data.type);
|
|
250
250
|
const interop = _optionalChain([descriptor, 'optionalAccess', _6 => _6.interop]);
|
|
251
251
|
if (interop === void 0) {
|
|
252
252
|
return {};
|
|
@@ -266,4 +266,4 @@ function xapiDefinitionFor(data) {
|
|
|
266
266
|
|
|
267
267
|
|
|
268
268
|
exports.validateXAPIStatement = validateXAPIStatement; exports.XAPIVerb = XAPIVerb; exports.XAPI_VERB_DISPLAY = XAPI_VERB_DISPLAY; exports.xAPIBuilder = xAPIBuilder; exports.xapiDefinitionFor = xapiDefinitionFor;
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
269
|
+
//# sourceMappingURL=chunk-64NBQCRE.cjs.map
|