@intellectif/lk-core 0.3.0 → 0.4.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/dist/{activity-zdcAMtFB.d.cts → activity-wkzRemHx.d.cts} +113 -1
- package/dist/{activity-zdcAMtFB.d.ts → activity-wkzRemHx.d.ts} +113 -1
- package/dist/chunk-5ZAW76F3.cjs +334 -0
- package/dist/chunk-5ZAW76F3.cjs.map +1 -0
- package/dist/chunk-A6JXV5GP.js +334 -0
- package/dist/chunk-A6JXV5GP.js.map +1 -0
- package/dist/{chunk-HS7BYCGE.js → chunk-APDLWLYD.js} +11 -3
- package/dist/chunk-APDLWLYD.js.map +1 -0
- package/dist/{chunk-RUGIOQZY.cjs → chunk-RUZ52Q7A.cjs} +11 -3
- package/dist/chunk-RUZ52Q7A.cjs.map +1 -0
- package/dist/index.cjs +17 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.cts +3 -3
- package/dist/schemas.d.ts +3 -3
- package/dist/scoring.cjs +10 -2
- package/dist/scoring.cjs.map +1 -1
- package/dist/scoring.d.cts +190 -3
- package/dist/scoring.d.ts +190 -3
- package/dist/scoring.js +9 -1
- package/dist/xapi.cjs +2 -2
- package/dist/xapi.d.cts +8 -1
- package/dist/xapi.d.ts +8 -1
- package/dist/xapi.js +1 -1
- package/package.json +37 -15
- package/dist/chunk-HS7BYCGE.js.map +0 -1
- package/dist/chunk-QSVBYTNM.js +0 -94
- package/dist/chunk-QSVBYTNM.js.map +0 -1
- package/dist/chunk-RUGIOQZY.cjs.map +0 -1
- package/dist/chunk-THSZMZND.cjs +0 -94
- package/dist/chunk-THSZMZND.cjs.map +0 -1
package/dist/scoring.d.cts
CHANGED
|
@@ -1,5 +1,192 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { T as TextMatchPolicy,
|
|
1
|
+
import { m as ItemOutcome, A as ActivityData, L as LearnerResponse, e as ActivityType, r as ScoringResult } from './activity-wkzRemHx.cjs';
|
|
2
|
+
export { T as TextMatchPolicy, s as TextMatchResult, P as levenshteinDistance, Q as matchText } from './activity-wkzRemHx.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rounding for assessment scores.
|
|
6
|
+
*
|
|
7
|
+
* Rounding is **two different operations** that must not share one policy, and
|
|
8
|
+
* conflating them silently inverts one of them:
|
|
9
|
+
*
|
|
10
|
+
* - **Grade rounding** decides the number a learner is shown and recorded
|
|
11
|
+
* against. It rounds to a fixed number of decimal places, conventionally
|
|
12
|
+
* half-up, so a learner shown "70%" is not recorded as a fail at 69.6.
|
|
13
|
+
* - **Band / level classification** decides which level someone is placed in.
|
|
14
|
+
* It deliberately **floors**: placing a learner above their real level is
|
|
15
|
+
* the more harmful error, so a band boundary must not be reached by
|
|
16
|
+
* rounding up.
|
|
17
|
+
*
|
|
18
|
+
* There is therefore no default `RoundingPolicy` anywhere in this SDK, and
|
|
19
|
+
* `dp` has no default either — an integrator's deliberate choice must never be
|
|
20
|
+
* supplied by us.
|
|
21
|
+
*/
|
|
22
|
+
/** How a value is rounded to `dp` decimal places. */
|
|
23
|
+
type RoundingMode = 'half-up' | 'half-even' | 'floor' | 'ceil';
|
|
24
|
+
/** A rounding policy. Both fields are required — the SDK never guesses either. */
|
|
25
|
+
interface RoundingPolicy {
|
|
26
|
+
mode: RoundingMode;
|
|
27
|
+
/** Decimal places. Load-bearing: `dp: 2` is what stops 69.6 becoming a fail at 70. */
|
|
28
|
+
dp: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rounds `value` to `policy.dp` places under `policy.mode`.
|
|
32
|
+
*
|
|
33
|
+
* Idempotent for every mode: rounding an already-rounded value returns it
|
|
34
|
+
* unchanged. That matters because a composed score is rounded once and then
|
|
35
|
+
* compared through {@link gte}, which rounds again.
|
|
36
|
+
*/
|
|
37
|
+
declare function roundGrade(value: number, policy: RoundingPolicy): number;
|
|
38
|
+
/**
|
|
39
|
+
* Threshold comparison that rounds **both sides** before comparing.
|
|
40
|
+
*
|
|
41
|
+
* Comparing a raw float against a rounded threshold is how a learner ends up
|
|
42
|
+
* shown one number and recorded against another. Rounding both sides — and
|
|
43
|
+
* allowing an epsilon — makes "what the learner sees" and "what the gradebook
|
|
44
|
+
* decides" the same comparison.
|
|
45
|
+
*/
|
|
46
|
+
declare function gte(value: number, threshold: number, policy: RoundingPolicy): boolean;
|
|
47
|
+
/** A named band with an inclusive lower bound, e.g. `{ name: 'B1', min: 0.6 }`. */
|
|
48
|
+
interface Band {
|
|
49
|
+
name: string;
|
|
50
|
+
/** Inclusive lower bound on the same scale as the value being classified. */
|
|
51
|
+
min: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Classifies a value into a band by **flooring**: the highest band whose `min`
|
|
55
|
+
* the value actually reaches. Deliberately does NOT round up to a boundary —
|
|
56
|
+
* over-placement is the more harmful error, so a learner just below a boundary
|
|
57
|
+
* stays below it.
|
|
58
|
+
*
|
|
59
|
+
* Returns `null` when the value reaches no band's minimum.
|
|
60
|
+
*/
|
|
61
|
+
declare function classifyBand(value: number, bands: readonly Band[]): Band | null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sectioned assessment scoring — weights, per-section thresholds, and an
|
|
65
|
+
* explicit reason when an attempt fails.
|
|
66
|
+
*
|
|
67
|
+
* This exists because the formula is invariably implemented twice: once on the
|
|
68
|
+
* server that records the grade, and again on the client that shows a learner
|
|
69
|
+
* their grade breakdown. Two implementations of one formula is exactly the
|
|
70
|
+
* duplication an SDK should remove, and they drift — usually in the scale
|
|
71
|
+
* (0–1 vs 0–100) or in whether a section override is honoured.
|
|
72
|
+
*/
|
|
73
|
+
/** One item's contribution to a section. */
|
|
74
|
+
interface ScoredItem {
|
|
75
|
+
/**
|
|
76
|
+
* Identity of the SLOT, not the activity. The same activity can legitimately
|
|
77
|
+
* appear in two sections; keying on the activity id collapses them into one
|
|
78
|
+
* and silently scores the second occurrence as zero.
|
|
79
|
+
*/
|
|
80
|
+
slotId: string;
|
|
81
|
+
/** The activity that filled this slot, for reporting. */
|
|
82
|
+
activityId?: string;
|
|
83
|
+
/** Maximum points this slot is worth. */
|
|
84
|
+
points: number;
|
|
85
|
+
/** What the learner achieved, or why there is no grade yet. */
|
|
86
|
+
outcome: ItemOutcome;
|
|
87
|
+
}
|
|
88
|
+
/** A weighted section of an assessment. */
|
|
89
|
+
interface AssessmentSectionInput {
|
|
90
|
+
id: string;
|
|
91
|
+
title?: string;
|
|
92
|
+
/** Relative weight. Weights are normalised by their sum, so they need not total 1. */
|
|
93
|
+
weight: number;
|
|
94
|
+
/** Overrides the assessment-wide section threshold for this section only. */
|
|
95
|
+
passThresholdOverride?: number;
|
|
96
|
+
items: ScoredItem[];
|
|
97
|
+
}
|
|
98
|
+
/** Policy for {@link composeAssessmentScore}. */
|
|
99
|
+
interface CompositionPolicy {
|
|
100
|
+
/** Scaled [0,1] overall score required to pass. */
|
|
101
|
+
passThreshold: number;
|
|
102
|
+
/** Scaled [0,1] score each section must reach, when sections gate the pass. */
|
|
103
|
+
sectionThreshold?: number;
|
|
104
|
+
/**
|
|
105
|
+
* How grades are rounded. Required, with no default: see `RoundingPolicy` —
|
|
106
|
+
* grade rounding and band classification are different operations and the
|
|
107
|
+
* SDK must not choose either for you.
|
|
108
|
+
*/
|
|
109
|
+
rounding: RoundingPolicy;
|
|
110
|
+
}
|
|
111
|
+
/** Per-section result. */
|
|
112
|
+
interface SectionScore {
|
|
113
|
+
id: string;
|
|
114
|
+
title?: string;
|
|
115
|
+
/** The authored weight, verbatim. */
|
|
116
|
+
weight: number;
|
|
117
|
+
/**
|
|
118
|
+
* The weight ACTUALLY used in the total, so a client can rebuild the grade
|
|
119
|
+
* from `sections[]` and agree with the record:
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* roundGrade(
|
|
123
|
+
* sections.reduce((sum, s) => sum + s.score * s.normalizedWeight, 0),
|
|
124
|
+
* policy.rounding,
|
|
125
|
+
* ) === result.score // exact, by construction
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Apply the same final rounding: the raw weighted sum of already-rounded
|
|
129
|
+
* section scores is not itself a rounded value (0.85 and 1.00 at equal
|
|
130
|
+
* weights sum to 0.925 against a recorded 0.93), so comparing it unrounded
|
|
131
|
+
* is off by up to half a quantum.
|
|
132
|
+
*
|
|
133
|
+
* Sections with nothing graded carry `0` here, because they contribute
|
|
134
|
+
* nothing; the remaining weights are renormalised among themselves.
|
|
135
|
+
*/
|
|
136
|
+
normalizedWeight: number;
|
|
137
|
+
earnedPoints: number;
|
|
138
|
+
/** Points that are currently gradable — excludes items still awaiting a grade. */
|
|
139
|
+
gradedMaxPoints: number;
|
|
140
|
+
/** Every point in the section, whether graded yet or not. */
|
|
141
|
+
maxPoints: number;
|
|
142
|
+
/** Scaled [0,1] over the GRADED points, rounded once. */
|
|
143
|
+
score: number;
|
|
144
|
+
passed: boolean;
|
|
145
|
+
/** Threshold this section was judged against, after any override. */
|
|
146
|
+
appliedThreshold: number | null;
|
|
147
|
+
/** Slots still awaiting a grade. */
|
|
148
|
+
pendingSlotIds: string[];
|
|
149
|
+
/** Slots that can never be graded, excluded from the denominator. */
|
|
150
|
+
unscorableSlotIds: string[];
|
|
151
|
+
}
|
|
152
|
+
/** Why an attempt failed, or `null` when it passed. */
|
|
153
|
+
type PassFailureReason = 'overall_below_threshold' | 'section_below_threshold' | 'both' | null;
|
|
154
|
+
/** Result of composing an assessment. */
|
|
155
|
+
interface AssessmentScore {
|
|
156
|
+
sections: SectionScore[];
|
|
157
|
+
/** Weighted total, scaled [0,1], rounded once. */
|
|
158
|
+
score: number;
|
|
159
|
+
/**
|
|
160
|
+
* Whether the attempt passed — `null` while `status` is `provisional`,
|
|
161
|
+
* because an attempt with work still ungraded has not passed OR failed yet.
|
|
162
|
+
* Returning `false` there would let a UI keyed on `passed` show a fail for
|
|
163
|
+
* an essay nobody has marked.
|
|
164
|
+
*/
|
|
165
|
+
passed: boolean | null;
|
|
166
|
+
/** `null` while provisional, for the same reason as {@link passed}. */
|
|
167
|
+
passFailureReason: PassFailureReason;
|
|
168
|
+
/**
|
|
169
|
+
* `provisional` while any item is still awaiting a grade — the total is
|
|
170
|
+
* computed over what HAS been graded, so it can still move. Do not record a
|
|
171
|
+
* provisional score as final. Items that can NEVER be graded
|
|
172
|
+
* (`unscorableSlotIds`) do not hold the result provisional.
|
|
173
|
+
*/
|
|
174
|
+
status: 'final' | 'provisional';
|
|
175
|
+
pendingSlotIds: string[];
|
|
176
|
+
/** Slots that can never be graded. Excluded from the denominator. */
|
|
177
|
+
unscorableSlotIds: string[];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Composes per-item outcomes into a sectioned assessment score.
|
|
181
|
+
*
|
|
182
|
+
* Weights are normalised by their sum. Each section's score is computed over
|
|
183
|
+
* the points that are actually gradable and rounded ONCE, before any threshold
|
|
184
|
+
* comparison, so the number a learner is shown is the number that decides the
|
|
185
|
+
* outcome. Items still awaiting a grade are excluded from the denominator
|
|
186
|
+
* rather than counted as zero, and the result is reported as `provisional`
|
|
187
|
+
* until every item has a grade.
|
|
188
|
+
*/
|
|
189
|
+
declare function composeAssessmentScore(sections: readonly AssessmentSectionInput[], policy: CompositionPolicy): AssessmentScore;
|
|
3
190
|
|
|
4
191
|
/** Default minimum scaled score required to pass when `passThreshold` is absent. */
|
|
5
192
|
declare const DEFAULT_PASS_THRESHOLD = 0.7;
|
|
@@ -41,4 +228,4 @@ declare function score(activityType: ActivityType, activityData: ActivityData, l
|
|
|
41
228
|
*/
|
|
42
229
|
declare function evaluate(data: ActivityData, response: LearnerResponse): ItemOutcome;
|
|
43
230
|
|
|
44
|
-
export { DEFAULT_PASS_THRESHOLD, computePassThreshold, evaluate, score };
|
|
231
|
+
export { type AssessmentScore, type AssessmentSectionInput, type Band, type CompositionPolicy, DEFAULT_PASS_THRESHOLD, type PassFailureReason, type RoundingMode, type RoundingPolicy, type ScoredItem, type SectionScore, classifyBand, composeAssessmentScore, computePassThreshold, evaluate, gte, roundGrade, score };
|
package/dist/scoring.d.ts
CHANGED
|
@@ -1,5 +1,192 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { T as TextMatchPolicy,
|
|
1
|
+
import { m as ItemOutcome, A as ActivityData, L as LearnerResponse, e as ActivityType, r as ScoringResult } from './activity-wkzRemHx.js';
|
|
2
|
+
export { T as TextMatchPolicy, s as TextMatchResult, P as levenshteinDistance, Q as matchText } from './activity-wkzRemHx.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rounding for assessment scores.
|
|
6
|
+
*
|
|
7
|
+
* Rounding is **two different operations** that must not share one policy, and
|
|
8
|
+
* conflating them silently inverts one of them:
|
|
9
|
+
*
|
|
10
|
+
* - **Grade rounding** decides the number a learner is shown and recorded
|
|
11
|
+
* against. It rounds to a fixed number of decimal places, conventionally
|
|
12
|
+
* half-up, so a learner shown "70%" is not recorded as a fail at 69.6.
|
|
13
|
+
* - **Band / level classification** decides which level someone is placed in.
|
|
14
|
+
* It deliberately **floors**: placing a learner above their real level is
|
|
15
|
+
* the more harmful error, so a band boundary must not be reached by
|
|
16
|
+
* rounding up.
|
|
17
|
+
*
|
|
18
|
+
* There is therefore no default `RoundingPolicy` anywhere in this SDK, and
|
|
19
|
+
* `dp` has no default either — an integrator's deliberate choice must never be
|
|
20
|
+
* supplied by us.
|
|
21
|
+
*/
|
|
22
|
+
/** How a value is rounded to `dp` decimal places. */
|
|
23
|
+
type RoundingMode = 'half-up' | 'half-even' | 'floor' | 'ceil';
|
|
24
|
+
/** A rounding policy. Both fields are required — the SDK never guesses either. */
|
|
25
|
+
interface RoundingPolicy {
|
|
26
|
+
mode: RoundingMode;
|
|
27
|
+
/** Decimal places. Load-bearing: `dp: 2` is what stops 69.6 becoming a fail at 70. */
|
|
28
|
+
dp: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rounds `value` to `policy.dp` places under `policy.mode`.
|
|
32
|
+
*
|
|
33
|
+
* Idempotent for every mode: rounding an already-rounded value returns it
|
|
34
|
+
* unchanged. That matters because a composed score is rounded once and then
|
|
35
|
+
* compared through {@link gte}, which rounds again.
|
|
36
|
+
*/
|
|
37
|
+
declare function roundGrade(value: number, policy: RoundingPolicy): number;
|
|
38
|
+
/**
|
|
39
|
+
* Threshold comparison that rounds **both sides** before comparing.
|
|
40
|
+
*
|
|
41
|
+
* Comparing a raw float against a rounded threshold is how a learner ends up
|
|
42
|
+
* shown one number and recorded against another. Rounding both sides — and
|
|
43
|
+
* allowing an epsilon — makes "what the learner sees" and "what the gradebook
|
|
44
|
+
* decides" the same comparison.
|
|
45
|
+
*/
|
|
46
|
+
declare function gte(value: number, threshold: number, policy: RoundingPolicy): boolean;
|
|
47
|
+
/** A named band with an inclusive lower bound, e.g. `{ name: 'B1', min: 0.6 }`. */
|
|
48
|
+
interface Band {
|
|
49
|
+
name: string;
|
|
50
|
+
/** Inclusive lower bound on the same scale as the value being classified. */
|
|
51
|
+
min: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Classifies a value into a band by **flooring**: the highest band whose `min`
|
|
55
|
+
* the value actually reaches. Deliberately does NOT round up to a boundary —
|
|
56
|
+
* over-placement is the more harmful error, so a learner just below a boundary
|
|
57
|
+
* stays below it.
|
|
58
|
+
*
|
|
59
|
+
* Returns `null` when the value reaches no band's minimum.
|
|
60
|
+
*/
|
|
61
|
+
declare function classifyBand(value: number, bands: readonly Band[]): Band | null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sectioned assessment scoring — weights, per-section thresholds, and an
|
|
65
|
+
* explicit reason when an attempt fails.
|
|
66
|
+
*
|
|
67
|
+
* This exists because the formula is invariably implemented twice: once on the
|
|
68
|
+
* server that records the grade, and again on the client that shows a learner
|
|
69
|
+
* their grade breakdown. Two implementations of one formula is exactly the
|
|
70
|
+
* duplication an SDK should remove, and they drift — usually in the scale
|
|
71
|
+
* (0–1 vs 0–100) or in whether a section override is honoured.
|
|
72
|
+
*/
|
|
73
|
+
/** One item's contribution to a section. */
|
|
74
|
+
interface ScoredItem {
|
|
75
|
+
/**
|
|
76
|
+
* Identity of the SLOT, not the activity. The same activity can legitimately
|
|
77
|
+
* appear in two sections; keying on the activity id collapses them into one
|
|
78
|
+
* and silently scores the second occurrence as zero.
|
|
79
|
+
*/
|
|
80
|
+
slotId: string;
|
|
81
|
+
/** The activity that filled this slot, for reporting. */
|
|
82
|
+
activityId?: string;
|
|
83
|
+
/** Maximum points this slot is worth. */
|
|
84
|
+
points: number;
|
|
85
|
+
/** What the learner achieved, or why there is no grade yet. */
|
|
86
|
+
outcome: ItemOutcome;
|
|
87
|
+
}
|
|
88
|
+
/** A weighted section of an assessment. */
|
|
89
|
+
interface AssessmentSectionInput {
|
|
90
|
+
id: string;
|
|
91
|
+
title?: string;
|
|
92
|
+
/** Relative weight. Weights are normalised by their sum, so they need not total 1. */
|
|
93
|
+
weight: number;
|
|
94
|
+
/** Overrides the assessment-wide section threshold for this section only. */
|
|
95
|
+
passThresholdOverride?: number;
|
|
96
|
+
items: ScoredItem[];
|
|
97
|
+
}
|
|
98
|
+
/** Policy for {@link composeAssessmentScore}. */
|
|
99
|
+
interface CompositionPolicy {
|
|
100
|
+
/** Scaled [0,1] overall score required to pass. */
|
|
101
|
+
passThreshold: number;
|
|
102
|
+
/** Scaled [0,1] score each section must reach, when sections gate the pass. */
|
|
103
|
+
sectionThreshold?: number;
|
|
104
|
+
/**
|
|
105
|
+
* How grades are rounded. Required, with no default: see `RoundingPolicy` —
|
|
106
|
+
* grade rounding and band classification are different operations and the
|
|
107
|
+
* SDK must not choose either for you.
|
|
108
|
+
*/
|
|
109
|
+
rounding: RoundingPolicy;
|
|
110
|
+
}
|
|
111
|
+
/** Per-section result. */
|
|
112
|
+
interface SectionScore {
|
|
113
|
+
id: string;
|
|
114
|
+
title?: string;
|
|
115
|
+
/** The authored weight, verbatim. */
|
|
116
|
+
weight: number;
|
|
117
|
+
/**
|
|
118
|
+
* The weight ACTUALLY used in the total, so a client can rebuild the grade
|
|
119
|
+
* from `sections[]` and agree with the record:
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* roundGrade(
|
|
123
|
+
* sections.reduce((sum, s) => sum + s.score * s.normalizedWeight, 0),
|
|
124
|
+
* policy.rounding,
|
|
125
|
+
* ) === result.score // exact, by construction
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Apply the same final rounding: the raw weighted sum of already-rounded
|
|
129
|
+
* section scores is not itself a rounded value (0.85 and 1.00 at equal
|
|
130
|
+
* weights sum to 0.925 against a recorded 0.93), so comparing it unrounded
|
|
131
|
+
* is off by up to half a quantum.
|
|
132
|
+
*
|
|
133
|
+
* Sections with nothing graded carry `0` here, because they contribute
|
|
134
|
+
* nothing; the remaining weights are renormalised among themselves.
|
|
135
|
+
*/
|
|
136
|
+
normalizedWeight: number;
|
|
137
|
+
earnedPoints: number;
|
|
138
|
+
/** Points that are currently gradable — excludes items still awaiting a grade. */
|
|
139
|
+
gradedMaxPoints: number;
|
|
140
|
+
/** Every point in the section, whether graded yet or not. */
|
|
141
|
+
maxPoints: number;
|
|
142
|
+
/** Scaled [0,1] over the GRADED points, rounded once. */
|
|
143
|
+
score: number;
|
|
144
|
+
passed: boolean;
|
|
145
|
+
/** Threshold this section was judged against, after any override. */
|
|
146
|
+
appliedThreshold: number | null;
|
|
147
|
+
/** Slots still awaiting a grade. */
|
|
148
|
+
pendingSlotIds: string[];
|
|
149
|
+
/** Slots that can never be graded, excluded from the denominator. */
|
|
150
|
+
unscorableSlotIds: string[];
|
|
151
|
+
}
|
|
152
|
+
/** Why an attempt failed, or `null` when it passed. */
|
|
153
|
+
type PassFailureReason = 'overall_below_threshold' | 'section_below_threshold' | 'both' | null;
|
|
154
|
+
/** Result of composing an assessment. */
|
|
155
|
+
interface AssessmentScore {
|
|
156
|
+
sections: SectionScore[];
|
|
157
|
+
/** Weighted total, scaled [0,1], rounded once. */
|
|
158
|
+
score: number;
|
|
159
|
+
/**
|
|
160
|
+
* Whether the attempt passed — `null` while `status` is `provisional`,
|
|
161
|
+
* because an attempt with work still ungraded has not passed OR failed yet.
|
|
162
|
+
* Returning `false` there would let a UI keyed on `passed` show a fail for
|
|
163
|
+
* an essay nobody has marked.
|
|
164
|
+
*/
|
|
165
|
+
passed: boolean | null;
|
|
166
|
+
/** `null` while provisional, for the same reason as {@link passed}. */
|
|
167
|
+
passFailureReason: PassFailureReason;
|
|
168
|
+
/**
|
|
169
|
+
* `provisional` while any item is still awaiting a grade — the total is
|
|
170
|
+
* computed over what HAS been graded, so it can still move. Do not record a
|
|
171
|
+
* provisional score as final. Items that can NEVER be graded
|
|
172
|
+
* (`unscorableSlotIds`) do not hold the result provisional.
|
|
173
|
+
*/
|
|
174
|
+
status: 'final' | 'provisional';
|
|
175
|
+
pendingSlotIds: string[];
|
|
176
|
+
/** Slots that can never be graded. Excluded from the denominator. */
|
|
177
|
+
unscorableSlotIds: string[];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Composes per-item outcomes into a sectioned assessment score.
|
|
181
|
+
*
|
|
182
|
+
* Weights are normalised by their sum. Each section's score is computed over
|
|
183
|
+
* the points that are actually gradable and rounded ONCE, before any threshold
|
|
184
|
+
* comparison, so the number a learner is shown is the number that decides the
|
|
185
|
+
* outcome. Items still awaiting a grade are excluded from the denominator
|
|
186
|
+
* rather than counted as zero, and the result is reported as `provisional`
|
|
187
|
+
* until every item has a grade.
|
|
188
|
+
*/
|
|
189
|
+
declare function composeAssessmentScore(sections: readonly AssessmentSectionInput[], policy: CompositionPolicy): AssessmentScore;
|
|
3
190
|
|
|
4
191
|
/** Default minimum scaled score required to pass when `passThreshold` is absent. */
|
|
5
192
|
declare const DEFAULT_PASS_THRESHOLD = 0.7;
|
|
@@ -41,4 +228,4 @@ declare function score(activityType: ActivityType, activityData: ActivityData, l
|
|
|
41
228
|
*/
|
|
42
229
|
declare function evaluate(data: ActivityData, response: LearnerResponse): ItemOutcome;
|
|
43
230
|
|
|
44
|
-
export { DEFAULT_PASS_THRESHOLD, computePassThreshold, evaluate, score };
|
|
231
|
+
export { type AssessmentScore, type AssessmentSectionInput, type Band, type CompositionPolicy, DEFAULT_PASS_THRESHOLD, type PassFailureReason, type RoundingMode, type RoundingPolicy, type ScoredItem, type SectionScore, classifyBand, composeAssessmentScore, computePassThreshold, evaluate, gte, roundGrade, score };
|
package/dist/scoring.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_PASS_THRESHOLD,
|
|
3
|
+
classifyBand,
|
|
4
|
+
composeAssessmentScore,
|
|
3
5
|
computePassThreshold,
|
|
4
6
|
evaluate,
|
|
7
|
+
gte,
|
|
8
|
+
roundGrade,
|
|
5
9
|
score
|
|
6
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-A6JXV5GP.js";
|
|
7
11
|
import {
|
|
8
12
|
levenshteinDistance,
|
|
9
13
|
matchText
|
|
@@ -11,10 +15,14 @@ import {
|
|
|
11
15
|
import "./chunk-3YAVDV5F.js";
|
|
12
16
|
export {
|
|
13
17
|
DEFAULT_PASS_THRESHOLD,
|
|
18
|
+
classifyBand,
|
|
19
|
+
composeAssessmentScore,
|
|
14
20
|
computePassThreshold,
|
|
15
21
|
evaluate,
|
|
22
|
+
gte,
|
|
16
23
|
levenshteinDistance,
|
|
17
24
|
matchText,
|
|
25
|
+
roundGrade,
|
|
18
26
|
score
|
|
19
27
|
};
|
|
20
28
|
//# sourceMappingURL=scoring.js.map
|
package/dist/xapi.cjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkRUZ52Q7Acjs = require('./chunk-RUZ52Q7A.cjs');
|
|
7
7
|
require('./chunk-PIMX4B4D.cjs');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
exports.XAPIVerb =
|
|
13
|
+
exports.XAPIVerb = _chunkRUZ52Q7Acjs.XAPIVerb; exports.XAPI_VERB_DISPLAY = _chunkRUZ52Q7Acjs.XAPI_VERB_DISPLAY; exports.validateXAPIStatement = _chunkRUZ52Q7Acjs.validateXAPIStatement; exports.xAPIBuilder = _chunkRUZ52Q7Acjs.xAPIBuilder;
|
|
14
14
|
//# sourceMappingURL=xapi.cjs.map
|
package/dist/xapi.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { X as XAPIActor,
|
|
1
|
+
import { X as XAPIActor, r as ScoringResult, y as XAPIContext, J as XAPIResult, N as XAPIStatement } from './activity-wkzRemHx.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The set of xAPI verbs emitted by SDK activities, keyed by a stable
|
|
@@ -21,6 +21,13 @@ declare const XAPIVerb: {
|
|
|
21
21
|
* community choice (design decision recorded per Task 25.4).
|
|
22
22
|
*/
|
|
23
23
|
readonly SUBMITTED: "http://activitystrea.ms/schema/1.0/submit";
|
|
24
|
+
/**
|
|
25
|
+
* Emitted when an asynchronous grader produces a grade for work that was
|
|
26
|
+
* previously only SUBMITTED. Distinct from `answered`, which asserts the
|
|
27
|
+
* grade existed at submission time — here the learner acted earlier and the
|
|
28
|
+
* grade arrived later, often from a different actor (an AI or a teacher).
|
|
29
|
+
*/
|
|
30
|
+
readonly SCORED: "http://adlnet.gov/expapi/verbs/scored";
|
|
24
31
|
};
|
|
25
32
|
/** Union of the valid `XAPIVerb` keys. */
|
|
26
33
|
type XAPIVerbKey = keyof typeof XAPIVerb;
|
package/dist/xapi.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { X as XAPIActor,
|
|
1
|
+
import { X as XAPIActor, r as ScoringResult, y as XAPIContext, J as XAPIResult, N as XAPIStatement } from './activity-wkzRemHx.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The set of xAPI verbs emitted by SDK activities, keyed by a stable
|
|
@@ -21,6 +21,13 @@ declare const XAPIVerb: {
|
|
|
21
21
|
* community choice (design decision recorded per Task 25.4).
|
|
22
22
|
*/
|
|
23
23
|
readonly SUBMITTED: "http://activitystrea.ms/schema/1.0/submit";
|
|
24
|
+
/**
|
|
25
|
+
* Emitted when an asynchronous grader produces a grade for work that was
|
|
26
|
+
* previously only SUBMITTED. Distinct from `answered`, which asserts the
|
|
27
|
+
* grade existed at submission time — here the learner acted earlier and the
|
|
28
|
+
* grade arrived later, often from a different actor (an AI or a teacher).
|
|
29
|
+
*/
|
|
30
|
+
readonly SCORED: "http://adlnet.gov/expapi/verbs/scored";
|
|
24
31
|
};
|
|
25
32
|
/** Union of the valid `XAPIVerb` keys. */
|
|
26
33
|
type XAPIVerbKey = keyof typeof XAPIVerb;
|
package/dist/xapi.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectif/lk-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Schemas, scoring engine, and xAPI builder for learning-kit",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,24 +10,44 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
16
21
|
},
|
|
17
22
|
"./scoring": {
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/scoring.d.ts",
|
|
25
|
+
"default": "./dist/scoring.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/scoring.d.cts",
|
|
29
|
+
"default": "./dist/scoring.cjs"
|
|
30
|
+
}
|
|
21
31
|
},
|
|
22
32
|
"./xapi": {
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/xapi.d.ts",
|
|
35
|
+
"default": "./dist/xapi.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/xapi.d.cts",
|
|
39
|
+
"default": "./dist/xapi.cjs"
|
|
40
|
+
}
|
|
26
41
|
},
|
|
27
42
|
"./schemas": {
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
"import": {
|
|
44
|
+
"types": "./dist/schemas.d.ts",
|
|
45
|
+
"default": "./dist/schemas.js"
|
|
46
|
+
},
|
|
47
|
+
"require": {
|
|
48
|
+
"types": "./dist/schemas.d.cts",
|
|
49
|
+
"default": "./dist/schemas.cjs"
|
|
50
|
+
}
|
|
31
51
|
}
|
|
32
52
|
},
|
|
33
53
|
"main": "./dist/index.cjs",
|
|
@@ -38,7 +58,7 @@
|
|
|
38
58
|
"LICENSE"
|
|
39
59
|
],
|
|
40
60
|
"dependencies": {
|
|
41
|
-
"zod": "^3.25.
|
|
61
|
+
"zod": "^3.25.1"
|
|
42
62
|
},
|
|
43
63
|
"devDependencies": {
|
|
44
64
|
"@vitest/coverage-v8": "^4.1.6",
|
|
@@ -55,6 +75,8 @@
|
|
|
55
75
|
"build": "tsup",
|
|
56
76
|
"test": "vitest run",
|
|
57
77
|
"coverage": "vitest run --coverage",
|
|
58
|
-
"lint": "biome check ."
|
|
78
|
+
"lint": "biome check .",
|
|
79
|
+
"publint": "publint",
|
|
80
|
+
"attw": "attw --pack . --ignore-rules no-resolution"
|
|
59
81
|
}
|
|
60
82
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/xapi/validators.ts","../src/xapi/verbs.ts","../src/xapi/builder.ts"],"sourcesContent":["import { z } from 'zod/v4';\nimport { ActivitySchemaError } from '../errors.js';\nimport type { ValidationError } from '../types/activity.js';\nimport type { XAPIStatement } from '../types/xapi.js';\n\nconst UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n// Minimal IRI/URI check: a scheme followed by ':'.\nconst IRI = /^[a-z][a-z0-9+.-]*:/i;\n\nconst langMap = z.record(z.string(), z.string());\nconst extensions = z.record(z.string(), z.unknown());\n\nconst actorSchema = z\n .object({\n objectType: z.literal('Agent'),\n name: z.string().optional(),\n mbox: z\n .string()\n .regex(/^mailto:/, 'mbox must be a mailto IRI')\n .optional(),\n account: z.object({ homePage: z.string(), name: z.string() }).optional(),\n })\n .refine((actor) => actor.mbox !== undefined || actor.account !== undefined, {\n error: 'actor must be identified by an mbox or an account',\n });\n\nconst statementSchema = z.object({\n id: z.string().regex(UUID_V4, 'id must be a UUID v4'),\n actor: actorSchema,\n verb: z.object({\n id: z.string().regex(IRI, 'verb.id must be an IRI'),\n display: langMap,\n }),\n object: z.object({\n objectType: z.literal('Activity'),\n id: z.string().regex(IRI, 'object.id must be an IRI'),\n definition: z\n .object({\n name: langMap.optional(),\n description: langMap.optional(),\n type: z.string().optional(),\n interactionType: z.string().optional(),\n correctResponsesPattern: z.array(z.string()).optional(),\n choices: z.array(z.object({ id: z.string(), description: langMap.optional() })).optional(),\n extensions: extensions.optional(),\n })\n .optional(),\n }),\n result: z\n .object({\n // xAPI 1.0.3 permits scaled in [-1, 1]; the SDK only emits [0, 1].\n score: z\n .object({\n scaled: z.number().min(-1).max(1),\n raw: z.number().optional(),\n min: z.number().optional(),\n max: z.number().optional(),\n })\n .optional(),\n success: z.boolean().optional(),\n completion: z.boolean().optional(),\n duration: z.string().regex(/^P/, 'duration must be an ISO 8601 duration').optional(),\n response: z.string().optional(),\n extensions: extensions.optional(),\n })\n .optional(),\n context: z\n .object({\n platform: z.string().optional(),\n language: z.string().optional(),\n contextActivities: z\n .object({\n parent: z\n .array(z.object({ objectType: z.literal('Activity'), id: z.string() }))\n .optional(),\n grouping: z\n .array(z.object({ objectType: z.literal('Activity'), id: z.string() }))\n .optional(),\n category: z\n .array(z.object({ objectType: z.literal('Activity'), id: z.string() }))\n .optional(),\n other: z\n .array(z.object({ objectType: z.literal('Activity'), id: z.string() }))\n .optional(),\n })\n .optional(),\n extensions: extensions.optional(),\n })\n .optional(),\n timestamp: z\n .string()\n .refine((value) => !Number.isNaN(Date.parse(value)), 'timestamp must be ISO 8601'),\n version: z.literal('1.0.3'),\n});\n\n/** True when running under `NODE_ENV=production`, safe in any environment. */\nfunction isProduction(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV === 'production';\n}\n\n/**\n * Validates a constructed {@link XAPIStatement} against the xAPI 1.0.3\n * structural contract.\n *\n * In development a structural failure throws (caught early); in production it\n * logs a warning and returns (the statement is still sent — see the design's\n * Error Handling table). Uses a `zod/v4` schema rather than `ajv` + an\n * external JSON Schema to keep `lk-core` dependency-free.\n *\n * NOTE: `ActivitySchemaError` is reused here per the task spec; its\n * `activityType` field carries the sentinel `'xAPIStatement'`. A dedicated\n * `XAPIValidationError` would be semantically cleaner — flagged for review.\n */\nexport function validateXAPIStatement(statement: XAPIStatement): void {\n const result = statementSchema.safeParse(statement);\n if (result.success) {\n return;\n }\n\n const errors: ValidationError[] = result.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n }));\n\n if (isProduction()) {\n console.warn('[learning-kit] Invalid xAPI statement (sent anyway):', errors);\n return;\n }\n\n throw new ActivitySchemaError('xAPIStatement', errors);\n}\n","/**\n * The set of xAPI verbs emitted by SDK activities, keyed by a stable\n * identifier mapped to its canonical xAPI verb IRI (xAPI 1.0.3 / ADL registry).\n */\nexport const XAPIVerb = {\n ANSWERED: 'http://adlnet.gov/expapi/verbs/answered',\n COMPLETED: 'http://adlnet.gov/expapi/verbs/completed',\n EXPERIENCED: 'http://adlnet.gov/expapi/verbs/experienced',\n INTERACTED: 'http://adlnet.gov/expapi/verbs/interacted',\n ATTEMPTED: 'http://adlnet.gov/expapi/verbs/attempted',\n PASSED: 'http://adlnet.gov/expapi/verbs/passed',\n FAILED: 'http://adlnet.gov/expapi/verbs/failed',\n WATCHED: 'https://w3id.org/xapi/video/verbs/watched',\n /**\n * Emitted for deferred-grading submissions (e.g. written-response): the\n * learner has submitted work whose grade does not exist yet, so `answered`\n * (which implies a scored response) would be wrong. xAPI 1.0.3 defines no\n * canonical \"submit\" verb; the Activity Streams 1.0 IRI is the established\n * community choice (design decision recorded per Task 25.4).\n */\n SUBMITTED: 'http://activitystrea.ms/schema/1.0/submit',\n} as const;\n\n/** Union of the valid `XAPIVerb` keys. */\nexport type XAPIVerbKey = keyof typeof XAPIVerb;\n\n/**\n * Default `en-US` display label for each verb, used to populate\n * `XAPIVerbObject.display` when the builder resolves a verb key.\n */\nexport const XAPI_VERB_DISPLAY: Record<XAPIVerbKey, Record<string, string>> = {\n ANSWERED: { 'en-US': 'answered' },\n COMPLETED: { 'en-US': 'completed' },\n EXPERIENCED: { 'en-US': 'experienced' },\n INTERACTED: { 'en-US': 'interacted' },\n ATTEMPTED: { 'en-US': 'attempted' },\n PASSED: { 'en-US': 'passed' },\n FAILED: { 'en-US': 'failed' },\n WATCHED: { 'en-US': 'watched' },\n SUBMITTED: { 'en-US': 'submitted' },\n};\n","import type { ScoringResult } from '../types/activity.js';\nimport type {\n XAPIActor,\n XAPIContext,\n XAPIObject,\n XAPIResult,\n XAPIStatement,\n} from '../types/xapi.js';\nimport { validateXAPIStatement } from './validators.js';\nimport { XAPI_VERB_DISPLAY, XAPIVerb, type XAPIVerbKey } from './verbs.js';\n\n/** Caller-supplied activity object descriptor (mapped to an xAPI Activity). */\nexport interface XAPIObjectParams {\n /** Activity IRI. */\n id: string;\n name?: Record<string, string>;\n description?: Record<string, string>;\n /** Activity type IRI. */\n type?: string;\n /** xAPI cmi.interaction type (e.g. `choice`, `fill-in`, `long-fill-in`). */\n interactionType?: string;\n /** Correct-responses patterns, per the xAPI CRP format. */\n correctResponsesPattern?: string[];\n /** For choice-family interactions: the available components. */\n choices?: Array<{ id: string; description?: Record<string, string> }>;\n}\n\n/** Parameters for the generic {@link xAPIBuilder.buildStatement}. */\nexport interface XAPIStatementParams {\n actor: XAPIActor;\n verb: XAPIVerbKey;\n object: XAPIObjectParams;\n result?: XAPIResult;\n context?: XAPIContext;\n}\n\n/** Parameters for {@link xAPIBuilder.buildAnsweredStatement} (verb = ANSWERED). */\nexport interface AnsweredStatementParams {\n actor: XAPIActor;\n object: XAPIObjectParams;\n scoringResult: ScoringResult;\n timeSpentMs: number;\n /** Serialized learner response string. */\n response?: string;\n context?: XAPIContext;\n resultExtensions?: Record<string, unknown>;\n}\n\n/**\n * Parameters for {@link xAPIBuilder.buildSubmittedStatement} (verb =\n * SUBMITTED — deferred-grading submissions). Carries NO score, success, or\n * completion: the grade does not exist yet (Req 22.7).\n */\nexport interface SubmittedStatementParams {\n actor: XAPIActor;\n object: XAPIObjectParams;\n timeSpentMs: number;\n /** Serialized learner response string (e.g. the submitted text). */\n response?: string;\n context?: XAPIContext;\n resultExtensions?: Record<string, unknown>;\n}\n\n/** Parameters for {@link xAPIBuilder.buildCompletedStatement} (verb = COMPLETED). */\nexport interface CompletedStatementParams {\n actor: XAPIActor;\n object: XAPIObjectParams;\n scoringResult?: ScoringResult;\n timeSpentMs: number;\n context?: XAPIContext;\n resultExtensions?: Record<string, unknown>;\n}\n\n/**\n * Converts a millisecond duration to an ISO 8601 seconds string with\n * centisecond precision (`PT1.23S`), per the xAPI 1.0.3 recommendation of\n * 0.01-second accuracy. Sub-second durations are preserved (previously any\n * duration under 500 ms collapsed to `PT0S`). Negative inputs (clock skew)\n * clamp to zero — a negative ISO duration is invalid.\n */\nfunction msToIsoDuration(ms: number): string {\n return `PT${(Math.round(Math.max(0, ms) / 10) / 100).toFixed(2)}S`;\n}\n\n/**\n * UUID v4 that works outside secure contexts. `crypto.randomUUID` is\n * SecureContext-gated (undefined on plain-http LAN/staging origins), and a\n * statement is built at SUBMIT time — throwing here would lose the learner's\n * answer. `crypto.getRandomValues` is NOT gated, so the fallback still emits\n * a spec-valid v4 UUID (the statement validator enforces the v4 format).\n */\nfunction uuidv4(): string {\n const c = (\n globalThis as {\n crypto?: { randomUUID?: () => string; getRandomValues?: (a: Uint8Array) => Uint8Array };\n }\n ).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n const bytes = new Uint8Array(16);\n if (c?.getRandomValues) {\n c.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n bytes[6] = ((bytes[6] as number) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80;\n const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/** Builds the xAPI Activity object, including `definition` only when populated. */\nfunction buildObject(params: XAPIObjectParams): XAPIObject {\n const definition: NonNullable<XAPIObject['definition']> = {\n ...(params.name !== undefined ? { name: params.name } : {}),\n ...(params.description !== undefined ? { description: params.description } : {}),\n ...(params.type !== undefined ? { type: params.type } : {}),\n ...(params.interactionType !== undefined ? { interactionType: params.interactionType } : {}),\n ...(params.correctResponsesPattern !== undefined\n ? { correctResponsesPattern: params.correctResponsesPattern }\n : {}),\n ...(params.choices !== undefined ? { choices: params.choices } : {}),\n };\n return {\n objectType: 'Activity',\n id: params.id,\n ...(Object.keys(definition).length > 0 ? { definition } : {}),\n };\n}\n\n/**\n * Maps a {@link ScoringResult} + timing to an {@link XAPIResult} per the\n * normative mapping in the design document.\n */\nfunction buildResult(\n timeSpentMs: number,\n scoringResult: ScoringResult | undefined,\n resultExtensions: Record<string, unknown> | undefined,\n response: string | undefined,\n): XAPIResult {\n return {\n completion: true,\n duration: msToIsoDuration(timeSpentMs),\n ...(scoringResult !== undefined\n ? {\n score: {\n scaled: scoringResult.score,\n raw: scoringResult.score,\n min: 0,\n max: scoringResult.maxScore,\n },\n success: scoringResult.passed,\n }\n : {}),\n ...(response !== undefined ? { response } : {}),\n ...(resultExtensions !== undefined ? { extensions: resultExtensions } : {}),\n };\n}\n\n/**\n * Constructs xAPI 1.0.3 Statements. Every call stamps a fresh UUID v4 `id`,\n * an ISO 8601 `timestamp`, and `version: '1.0.3'`. Not pure by design —\n * statements are unique events.\n */\nexport const xAPIBuilder = {\n buildStatement(params: XAPIStatementParams): XAPIStatement {\n const statement: XAPIStatement = {\n id: uuidv4(),\n actor: params.actor,\n verb: {\n id: XAPIVerb[params.verb],\n display: XAPI_VERB_DISPLAY[params.verb],\n },\n object: buildObject(params.object),\n ...(params.result !== undefined ? { result: params.result } : {}),\n ...(params.context !== undefined ? { context: params.context } : {}),\n timestamp: new Date().toISOString(),\n version: '1.0.3',\n };\n // Design xAPIBuilder step 4: validate every statement (throws in dev,\n // warns in prod). All build*Statement helpers route through here.\n validateXAPIStatement(statement);\n return statement;\n },\n\n buildAnsweredStatement(params: AnsweredStatementParams): XAPIStatement {\n return this.buildStatement({\n actor: params.actor,\n verb: 'ANSWERED',\n object: params.object,\n result: buildResult(\n params.timeSpentMs,\n params.scoringResult,\n params.resultExtensions,\n params.response,\n ),\n ...(params.context !== undefined ? { context: params.context } : {}),\n });\n },\n\n /**\n * Builds a statement for a deferred-grading submission (e.g. a written\n * response). Verb is SUBMITTED and the result deliberately omits `score`,\n * `success`, and `completion` — none of them exist until the asynchronous\n * grader runs; only `duration`, `response`, and extensions are recorded.\n */\n buildSubmittedStatement(params: SubmittedStatementParams): XAPIStatement {\n return this.buildStatement({\n actor: params.actor,\n verb: 'SUBMITTED',\n object: params.object,\n result: {\n duration: msToIsoDuration(params.timeSpentMs),\n ...(params.response !== undefined ? { response: params.response } : {}),\n ...(params.resultExtensions !== undefined ? { extensions: params.resultExtensions } : {}),\n },\n ...(params.context !== undefined ? { context: params.context } : {}),\n });\n },\n\n buildCompletedStatement(params: CompletedStatementParams): XAPIStatement {\n return this.buildStatement({\n actor: params.actor,\n verb: 'COMPLETED',\n object: params.object,\n result: buildResult(\n params.timeSpentMs,\n params.scoringResult,\n params.resultExtensions,\n undefined,\n ),\n ...(params.context !== undefined ? { context: params.context } : {}),\n });\n },\n};\n"],"mappings":"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","names":[]}
|