@intellectif/lk-core 0.3.1 → 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.
@@ -63,6 +63,104 @@ declare function levenshteinDistance(a: string, b: string, max: number): number;
63
63
  */
64
64
  declare function matchText(input: string, accepted: string | readonly string[], policy?: TextMatchPolicy): TextMatchResult;
65
65
 
66
+ /**
67
+ * The return trip for deferred grading.
68
+ *
69
+ * `evaluate()` can say a submission is `deferred` — graded later by an AI or a
70
+ * human — but until now there was no type for the grade that comes BACK, so
71
+ * every consumer invented one. The shapes here are the intersection of two
72
+ * independent production graders (a rubric-based essay grader and a CEFR
73
+ * speaking grader) that converged on the same envelope: a normalised total,
74
+ * per-criterion scores with comments, grader-authored artefacts, a confidence
75
+ * signal, an explicit human-review flag, and provenance.
76
+ *
77
+ * The SDK models the SHAPE and the arithmetic. It never calls a model, never
78
+ * holds a key, and never decides what a rubric means — that stays yours.
79
+ */
80
+ /**
81
+ * Lifecycle of a deferred grade. A grade exists only in the `graded` state;
82
+ * every other state means "no grade, and here is why" — which is precisely the
83
+ * distinction that stops an ungraded submission being rendered as a zero.
84
+ */
85
+ type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
86
+ /** Who produced a grade. */
87
+ type GraderKind = 'ai' | 'human' | 'auto';
88
+ /** Provenance of a grade, so a re-grade two years later is explicable. */
89
+ interface Grader {
90
+ kind: GraderKind;
91
+ /** Identifier of the human grader, when `kind` is `'human'`. */
92
+ id?: string;
93
+ /** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
94
+ model?: string;
95
+ /** Hash or id of the prompt/rubric revision used, for auditability. */
96
+ promptHash?: string;
97
+ }
98
+ /** Optional cost/usage telemetry emitted by an AI grader. */
99
+ interface GraderUsage {
100
+ promptTokens?: number;
101
+ completionTokens?: number;
102
+ costUsd?: number;
103
+ }
104
+ /** A grader's verdict on one rubric criterion. */
105
+ interface CriterionScore {
106
+ /** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
107
+ name: string;
108
+ /** Scaled [0,1]. Absent when the criterion is judged on an ordinal `band`. */
109
+ score?: number;
110
+ /** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
111
+ band?: string;
112
+ /** The grader's comment for this criterion, addressed to the learner. */
113
+ comment?: string;
114
+ /** Weight actually applied, echoed from the rubric so the total is checkable. */
115
+ weight?: number;
116
+ /** Not applicable to this submission (e.g. interaction on a monologue task). */
117
+ notApplicable?: boolean;
118
+ }
119
+ /** A suggested correction anchored in the learner's own text. */
120
+ interface InlineCorrection {
121
+ /** Verbatim span from the submission. */
122
+ original: string;
123
+ /** Suggested replacement. */
124
+ corrected: string;
125
+ explanation?: string;
126
+ /** Character offsets into the submitted text, so a UI can anchor it inline. */
127
+ range?: {
128
+ start: number;
129
+ end: number;
130
+ };
131
+ /** Error-type tag (`"article"`, `"tense"`, `"register"`). */
132
+ category?: string;
133
+ }
134
+ /**
135
+ * A grade that came back from an asynchronous grader. `score` is scaled
136
+ * [0,1] against `maxScore`, matching every other score in the SDK, so a
137
+ * grader that works in points must normalise before handing one over
138
+ * (or set `maxScore` accordingly).
139
+ */
140
+ interface GradeRecord {
141
+ score: number;
142
+ maxScore: number;
143
+ passed: boolean;
144
+ /** Narrative feedback addressed to the learner. */
145
+ feedback: string | null;
146
+ /** Per-criterion breakdown, when the grader worked against a rubric. */
147
+ criteria?: CriterionScore[];
148
+ /** Corrections anchored in the learner's text. */
149
+ corrections?: InlineCorrection[];
150
+ /** Supporting observations the grader cited. */
151
+ evidence?: string[];
152
+ /** How the grader reached this verdict. */
153
+ rationale?: string;
154
+ /** The grader's own confidence — a signal for routing to human review. */
155
+ confidence?: 'high' | 'medium' | 'low';
156
+ /** The grader is unsure or the submission is atypical; route to a human. */
157
+ requiresHumanReview?: boolean;
158
+ grader?: Grader;
159
+ usage?: GraderUsage;
160
+ /** ISO 8601 timestamp of when the grade was produced. */
161
+ gradedAt?: string;
162
+ }
163
+
66
164
  /**
67
165
  * An xAPI Activity object — the thing a statement is about.
68
166
  * This SDK only builds Activity-type objects (xAPI 1.0.3 §4.1.4.1).
@@ -564,6 +662,20 @@ type ItemOutcome = {
564
662
  maxScore: number;
565
663
  /** Synchronously computable progress facts (word bounds, counts). */
566
664
  partial?: DeferredScoringPartial;
665
+ } | {
666
+ status: 'graded';
667
+ /**
668
+ * A grade produced by an asynchronous grader (AI or human) and handed
669
+ * back to the SDK. This is the state a `deferred` outcome transitions
670
+ * to once grading completes; nothing in the SDK ever manufactures it.
671
+ */
672
+ grade: GradeRecord;
673
+ /** Scaled score in the range [0, 1], mirrored from `grade` for uniform reads. */
674
+ score: number;
675
+ maxScore: number;
676
+ passed: boolean;
677
+ /** Narrative feedback from the grader, mirrored from `grade`. */
678
+ feedback: string | null;
567
679
  } | {
568
680
  status: 'unscorable';
569
681
  /** Why no grade can be produced (e.g. unregistered activity type). */
@@ -623,4 +735,4 @@ interface InteractionEvent {
623
735
  payload: Record<string, unknown>;
624
736
  }
625
737
 
626
- export { type ActivityData as A, type BlankConfig as B, type XAPIVerbObject as C, type DeferredScoringPartial as D, levenshteinDistance as E, type FillInTheBlanksData as F, matchText as G, type InteractionEvent as I, type LearnerResponse as L, type MultipleChoiceData as M, 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 InteractionKind as g, type ItemOutcome as h, type LearnerResponseMap as i, type MultipleChoiceLearnerResponse as j, type MultipleChoiceOption as k, type ScoringOutcome as l, type ScoringResult as m, type TextMatchResult as n, type ValidationResult as o, type WrittenResponseLearnerResponse as p, type WrittenResponseRubric as q, type WrittenResponseRubricCriterion as r, type XAPIConfig as s, type XAPIContext as t, type XAPIContextActivities as u, type XAPIError as v, type XAPIObject as w, type XAPIResult as x, type XAPIScore as y, type XAPIStatement as z };
738
+ 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 XAPIStatement as N, type XAPIVerbObject as O, levenshteinDistance as P, matchText as Q, 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 };
@@ -63,6 +63,104 @@ declare function levenshteinDistance(a: string, b: string, max: number): number;
63
63
  */
64
64
  declare function matchText(input: string, accepted: string | readonly string[], policy?: TextMatchPolicy): TextMatchResult;
65
65
 
66
+ /**
67
+ * The return trip for deferred grading.
68
+ *
69
+ * `evaluate()` can say a submission is `deferred` — graded later by an AI or a
70
+ * human — but until now there was no type for the grade that comes BACK, so
71
+ * every consumer invented one. The shapes here are the intersection of two
72
+ * independent production graders (a rubric-based essay grader and a CEFR
73
+ * speaking grader) that converged on the same envelope: a normalised total,
74
+ * per-criterion scores with comments, grader-authored artefacts, a confidence
75
+ * signal, an explicit human-review flag, and provenance.
76
+ *
77
+ * The SDK models the SHAPE and the arithmetic. It never calls a model, never
78
+ * holds a key, and never decides what a rubric means — that stays yours.
79
+ */
80
+ /**
81
+ * Lifecycle of a deferred grade. A grade exists only in the `graded` state;
82
+ * every other state means "no grade, and here is why" — which is precisely the
83
+ * distinction that stops an ungraded submission being rendered as a zero.
84
+ */
85
+ type GradingState = 'queued' | 'running' | 'graded' | 'failed' | 'skipped';
86
+ /** Who produced a grade. */
87
+ type GraderKind = 'ai' | 'human' | 'auto';
88
+ /** Provenance of a grade, so a re-grade two years later is explicable. */
89
+ interface Grader {
90
+ kind: GraderKind;
91
+ /** Identifier of the human grader, when `kind` is `'human'`. */
92
+ id?: string;
93
+ /** Model identifier, when `kind` is `'ai'` (e.g. a model name and version). */
94
+ model?: string;
95
+ /** Hash or id of the prompt/rubric revision used, for auditability. */
96
+ promptHash?: string;
97
+ }
98
+ /** Optional cost/usage telemetry emitted by an AI grader. */
99
+ interface GraderUsage {
100
+ promptTokens?: number;
101
+ completionTokens?: number;
102
+ costUsd?: number;
103
+ }
104
+ /** A grader's verdict on one rubric criterion. */
105
+ interface CriterionScore {
106
+ /** Matches a `WrittenResponseRubricCriterion.name` when a rubric is known. */
107
+ name: string;
108
+ /** Scaled [0,1]. Absent when the criterion is judged on an ordinal `band`. */
109
+ score?: number;
110
+ /** Ordinal verdict when the rubric is banded rather than numeric (e.g. `"B1"`). */
111
+ band?: string;
112
+ /** The grader's comment for this criterion, addressed to the learner. */
113
+ comment?: string;
114
+ /** Weight actually applied, echoed from the rubric so the total is checkable. */
115
+ weight?: number;
116
+ /** Not applicable to this submission (e.g. interaction on a monologue task). */
117
+ notApplicable?: boolean;
118
+ }
119
+ /** A suggested correction anchored in the learner's own text. */
120
+ interface InlineCorrection {
121
+ /** Verbatim span from the submission. */
122
+ original: string;
123
+ /** Suggested replacement. */
124
+ corrected: string;
125
+ explanation?: string;
126
+ /** Character offsets into the submitted text, so a UI can anchor it inline. */
127
+ range?: {
128
+ start: number;
129
+ end: number;
130
+ };
131
+ /** Error-type tag (`"article"`, `"tense"`, `"register"`). */
132
+ category?: string;
133
+ }
134
+ /**
135
+ * A grade that came back from an asynchronous grader. `score` is scaled
136
+ * [0,1] against `maxScore`, matching every other score in the SDK, so a
137
+ * grader that works in points must normalise before handing one over
138
+ * (or set `maxScore` accordingly).
139
+ */
140
+ interface GradeRecord {
141
+ score: number;
142
+ maxScore: number;
143
+ passed: boolean;
144
+ /** Narrative feedback addressed to the learner. */
145
+ feedback: string | null;
146
+ /** Per-criterion breakdown, when the grader worked against a rubric. */
147
+ criteria?: CriterionScore[];
148
+ /** Corrections anchored in the learner's text. */
149
+ corrections?: InlineCorrection[];
150
+ /** Supporting observations the grader cited. */
151
+ evidence?: string[];
152
+ /** How the grader reached this verdict. */
153
+ rationale?: string;
154
+ /** The grader's own confidence — a signal for routing to human review. */
155
+ confidence?: 'high' | 'medium' | 'low';
156
+ /** The grader is unsure or the submission is atypical; route to a human. */
157
+ requiresHumanReview?: boolean;
158
+ grader?: Grader;
159
+ usage?: GraderUsage;
160
+ /** ISO 8601 timestamp of when the grade was produced. */
161
+ gradedAt?: string;
162
+ }
163
+
66
164
  /**
67
165
  * An xAPI Activity object — the thing a statement is about.
68
166
  * This SDK only builds Activity-type objects (xAPI 1.0.3 §4.1.4.1).
@@ -564,6 +662,20 @@ type ItemOutcome = {
564
662
  maxScore: number;
565
663
  /** Synchronously computable progress facts (word bounds, counts). */
566
664
  partial?: DeferredScoringPartial;
665
+ } | {
666
+ status: 'graded';
667
+ /**
668
+ * A grade produced by an asynchronous grader (AI or human) and handed
669
+ * back to the SDK. This is the state a `deferred` outcome transitions
670
+ * to once grading completes; nothing in the SDK ever manufactures it.
671
+ */
672
+ grade: GradeRecord;
673
+ /** Scaled score in the range [0, 1], mirrored from `grade` for uniform reads. */
674
+ score: number;
675
+ maxScore: number;
676
+ passed: boolean;
677
+ /** Narrative feedback from the grader, mirrored from `grade`. */
678
+ feedback: string | null;
567
679
  } | {
568
680
  status: 'unscorable';
569
681
  /** Why no grade can be produced (e.g. unregistered activity type). */
@@ -623,4 +735,4 @@ interface InteractionEvent {
623
735
  payload: Record<string, unknown>;
624
736
  }
625
737
 
626
- export { type ActivityData as A, type BlankConfig as B, type XAPIVerbObject as C, type DeferredScoringPartial as D, levenshteinDistance as E, type FillInTheBlanksData as F, matchText as G, type InteractionEvent as I, type LearnerResponse as L, type MultipleChoiceData as M, 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 InteractionKind as g, type ItemOutcome as h, type LearnerResponseMap as i, type MultipleChoiceLearnerResponse as j, type MultipleChoiceOption as k, type ScoringOutcome as l, type ScoringResult as m, type TextMatchResult as n, type ValidationResult as o, type WrittenResponseLearnerResponse as p, type WrittenResponseRubric as q, type WrittenResponseRubricCriterion as r, type XAPIConfig as s, type XAPIContext as t, type XAPIContextActivities as u, type XAPIError as v, type XAPIObject as w, type XAPIResult as x, type XAPIScore as y, type XAPIStatement as z };
738
+ 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 XAPIStatement as N, type XAPIVerbObject as O, levenshteinDistance as P, matchText as Q, 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 };
@@ -0,0 +1,334 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } 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
+
3
+ var _chunkQOAORTA4cjs = require('./chunk-QOAORTA4.cjs');
4
+
5
+
6
+
7
+
8
+ var _chunkPIMX4B4Dcjs = require('./chunk-PIMX4B4D.cjs');
9
+
10
+ // src/grading.ts
11
+ function gradeFromRubric(criteria, activityData, options = {}) {
12
+ const corrupt = criteria.find(
13
+ (criterion) => criterion.notApplicable !== true && criterion.score !== void 0 && !Number.isFinite(criterion.score)
14
+ );
15
+ if (corrupt !== void 0) {
16
+ return {
17
+ unscorable: true,
18
+ reason: `Criterion "${corrupt.name}" has a non-finite score (${String(corrupt.score)}). A grade cannot be computed from it.`
19
+ };
20
+ }
21
+ const scoreable = criteria.filter(
22
+ (criterion) => criterion.notApplicable !== true && typeof criterion.score === "number"
23
+ );
24
+ const EPSILON2 = 1e-9;
25
+ const outOfRange = scoreable.find(
26
+ (criterion) => criterion.score < -EPSILON2 || criterion.score > 1 + EPSILON2
27
+ );
28
+ if (outOfRange !== void 0) {
29
+ return {
30
+ unscorable: true,
31
+ reason: `Criterion "${outOfRange.name}" has score ${outOfRange.score}, outside the scaled [0,1] range. Normalise grader output before building a GradeRecord.`
32
+ };
33
+ }
34
+ if (scoreable.length === 0) {
35
+ return {
36
+ unscorable: true,
37
+ reason: "No criterion carried a numeric score, so no weighted total can be computed."
38
+ };
39
+ }
40
+ const totalWeight = scoreable.reduce((sum, criterion) => sum + (_nullishCoalesce(criterion.weight, () => ( 1))), 0);
41
+ if (!(totalWeight > 0)) {
42
+ return {
43
+ unscorable: true,
44
+ reason: "Criterion weights do not sum to a positive number, so the weighted total is undefined."
45
+ };
46
+ }
47
+ const weighted = scoreable.reduce(
48
+ (sum, criterion) => sum + criterion.score * (_nullishCoalesce(criterion.weight, () => ( 1))),
49
+ 0
50
+ );
51
+ const rawScore = weighted / totalWeight;
52
+ if (!Number.isFinite(rawScore)) {
53
+ return {
54
+ unscorable: true,
55
+ reason: "The weighted total is not a finite number, so no grade can be produced."
56
+ };
57
+ }
58
+ const score2 = Math.min(1, Math.max(0, rawScore));
59
+ const passed = options.passThreshold !== void 0 ? score2 >= options.passThreshold : activityData !== void 0 ? computePassThreshold(activityData, score2) : score2 >= 0.7;
60
+ return {
61
+ score: score2,
62
+ maxScore: 1,
63
+ passed,
64
+ feedback: _nullishCoalesce(options.feedback, () => ( null)),
65
+ criteria: [...criteria]
66
+ };
67
+ }
68
+ function outcomeFromGrade(grade) {
69
+ return {
70
+ status: "graded",
71
+ grade,
72
+ score: grade.score,
73
+ maxScore: grade.maxScore,
74
+ passed: grade.passed,
75
+ feedback: grade.feedback
76
+ };
77
+ }
78
+ function hasGrade(outcome) {
79
+ return outcome.status === "scored" || outcome.status === "graded";
80
+ }
81
+
82
+ // src/scoring/rounding.ts
83
+ var EPSILON = 1e-9;
84
+ var TIE_TOLERANCE = 1e-6;
85
+ function scaled(value, dp) {
86
+ return value * 10 ** dp;
87
+ }
88
+ function noNegZero(value) {
89
+ return Object.is(value, -0) ? 0 : value;
90
+ }
91
+ function roundGrade(value, policy) {
92
+ if (!Number.isFinite(value)) {
93
+ return value;
94
+ }
95
+ const factor = 10 ** policy.dp;
96
+ const raw = scaled(value, policy.dp);
97
+ switch (policy.mode) {
98
+ case "floor":
99
+ return noNegZero(Math.floor(raw + EPSILON) / factor);
100
+ case "ceil":
101
+ return noNegZero(Math.ceil(raw - EPSILON) / factor);
102
+ case "half-even": {
103
+ const floored = Math.floor(raw);
104
+ if (Math.abs(raw - floored - 0.5) > TIE_TOLERANCE) {
105
+ return noNegZero(halfAway(raw) / factor);
106
+ }
107
+ const even = floored % 2 === 0 ? floored : floored + 1;
108
+ return noNegZero(even / factor);
109
+ }
110
+ default:
111
+ return noNegZero(halfAway(raw) / factor);
112
+ }
113
+ }
114
+ function halfAway(raw) {
115
+ const nudged = raw >= 0 ? raw + EPSILON : raw - EPSILON;
116
+ return raw >= 0 ? Math.round(nudged) : -Math.round(-nudged);
117
+ }
118
+ function gte(value, threshold, policy) {
119
+ return roundGrade(value, policy) >= roundGrade(threshold, policy) - EPSILON;
120
+ }
121
+ function classifyBand(value, bands) {
122
+ let best = null;
123
+ for (const band of bands) {
124
+ if (value >= band.min - EPSILON && (best === null || band.min > best.min)) {
125
+ best = band;
126
+ }
127
+ }
128
+ return best;
129
+ }
130
+
131
+ // src/scoring/compose.ts
132
+ function earned(item) {
133
+ const { outcome } = item;
134
+ if (hasGrade(outcome)) {
135
+ const max = outcome.maxScore > 0 ? outcome.maxScore : 1;
136
+ return { state: "graded", points: outcome.score / max * item.points };
137
+ }
138
+ if (outcome.status === "deferred") {
139
+ return { state: "pending" };
140
+ }
141
+ return { state: "unscorable" };
142
+ }
143
+ function composeAssessmentScore(sections, policy) {
144
+ const pendingAll = [];
145
+ const unscorableAll = [];
146
+ const partials = sections.map((section) => {
147
+ let earnedPoints = 0;
148
+ let gradedMaxPoints = 0;
149
+ let maxPoints = 0;
150
+ const pendingSlotIds = [];
151
+ const unscorableSlotIds = [];
152
+ for (const item of section.items) {
153
+ maxPoints += item.points;
154
+ const contribution = earned(item);
155
+ if (contribution.state === "pending") {
156
+ pendingSlotIds.push(item.slotId);
157
+ pendingAll.push(item.slotId);
158
+ continue;
159
+ }
160
+ if (contribution.state === "unscorable") {
161
+ unscorableSlotIds.push(item.slotId);
162
+ unscorableAll.push(item.slotId);
163
+ continue;
164
+ }
165
+ earnedPoints += contribution.points;
166
+ gradedMaxPoints += item.points;
167
+ }
168
+ const raw = gradedMaxPoints > 0 ? earnedPoints / gradedMaxPoints : 0;
169
+ const appliedThreshold = _nullishCoalesce(_nullishCoalesce(section.passThresholdOverride, () => ( policy.sectionThreshold)), () => ( null));
170
+ return {
171
+ section,
172
+ earnedPoints,
173
+ gradedMaxPoints,
174
+ maxPoints,
175
+ score: roundGrade(raw, policy.rounding),
176
+ appliedThreshold,
177
+ pendingSlotIds,
178
+ unscorableSlotIds
179
+ };
180
+ });
181
+ const contributingWeight = partials.reduce(
182
+ (sum, partial) => sum + (partial.gradedMaxPoints > 0 ? partial.section.weight : 0),
183
+ 0
184
+ );
185
+ const scored = partials.map((partial) => ({
186
+ id: partial.section.id,
187
+ ...partial.section.title !== void 0 ? { title: partial.section.title } : {},
188
+ weight: partial.section.weight,
189
+ normalizedWeight: partial.gradedMaxPoints > 0 && contributingWeight > 0 ? partial.section.weight / contributingWeight : 0,
190
+ earnedPoints: partial.earnedPoints,
191
+ gradedMaxPoints: partial.gradedMaxPoints,
192
+ maxPoints: partial.maxPoints,
193
+ score: partial.score,
194
+ // A section with nothing graded yet cannot be said to have failed.
195
+ passed: partial.appliedThreshold === null || partial.gradedMaxPoints === 0 ? true : gte(partial.score, partial.appliedThreshold, policy.rounding),
196
+ appliedThreshold: partial.appliedThreshold,
197
+ pendingSlotIds: partial.pendingSlotIds,
198
+ unscorableSlotIds: partial.unscorableSlotIds
199
+ }));
200
+ const weightedRaw = scored.reduce(
201
+ (sum, section) => sum + section.score * section.normalizedWeight,
202
+ 0
203
+ );
204
+ const score2 = roundGrade(weightedRaw, policy.rounding);
205
+ const status = pendingAll.length > 0 ? "provisional" : "final";
206
+ if (status === "provisional") {
207
+ return {
208
+ sections: scored,
209
+ score: score2,
210
+ passed: null,
211
+ passFailureReason: null,
212
+ status,
213
+ pendingSlotIds: pendingAll,
214
+ unscorableSlotIds: unscorableAll
215
+ };
216
+ }
217
+ if (scored.every((section) => section.gradedMaxPoints === 0)) {
218
+ return {
219
+ sections: scored,
220
+ score: score2,
221
+ passed: null,
222
+ passFailureReason: null,
223
+ status,
224
+ pendingSlotIds: pendingAll,
225
+ unscorableSlotIds: unscorableAll
226
+ };
227
+ }
228
+ const overallOk = gte(score2, policy.passThreshold, policy.rounding);
229
+ const sectionsOk = scored.every((section) => section.passed);
230
+ const passed = overallOk && sectionsOk;
231
+ const passFailureReason = passed ? null : !overallOk && !sectionsOk ? "both" : overallOk ? "section_below_threshold" : "overall_below_threshold";
232
+ return {
233
+ sections: scored,
234
+ score: score2,
235
+ passed,
236
+ passFailureReason,
237
+ status,
238
+ pendingSlotIds: pendingAll,
239
+ unscorableSlotIds: unscorableAll
240
+ };
241
+ }
242
+
243
+ // src/scoring/index.ts
244
+ var DEFAULT_PASS_THRESHOLD = 0.7;
245
+ function computePassThreshold(activityData, score2) {
246
+ return score2 >= (_nullishCoalesce(activityData.passThreshold, () => ( DEFAULT_PASS_THRESHOLD)));
247
+ }
248
+ function isRedacted(data) {
249
+ return typeof data === "object" && data !== null && data.redacted === true;
250
+ }
251
+ function selectFeedback(activityData, passed) {
252
+ const feedback = activityData.feedback;
253
+ if (feedback === void 0) {
254
+ return null;
255
+ }
256
+ return _nullishCoalesce((passed ? feedback.correct : feedback.incorrect), () => ( null));
257
+ }
258
+ function score(activityType, activityData, learnerResponse) {
259
+ const descriptor = _chunkQOAORTA4cjs.getActivityTypeDescriptor.call(void 0, activityType);
260
+ if (descriptor === void 0) {
261
+ throw new (0, _chunkPIMX4B4Dcjs.UnknownActivityTypeError)(String(activityType));
262
+ }
263
+ if (descriptor.scoring.kind === "deferred") {
264
+ throw new (0, _chunkPIMX4B4Dcjs.DeferredScoringError)(descriptor.type);
265
+ }
266
+ if (isRedacted(activityData)) {
267
+ throw new (0, _chunkPIMX4B4Dcjs.RedactedScoringError)(descriptor.type);
268
+ }
269
+ const result = descriptor.scoring.score(activityData, learnerResponse);
270
+ if (!Number.isFinite(result.score)) {
271
+ throw new (0, _chunkPIMX4B4Dcjs.RedactedScoringError)(descriptor.type);
272
+ }
273
+ const passed = computePassThreshold(activityData, result.score);
274
+ return { ...result, passed, feedback: _nullishCoalesce(result.feedback, () => ( selectFeedback(activityData, passed))) };
275
+ }
276
+ function evaluate(data, response) {
277
+ const type = data.type;
278
+ const descriptor = typeof type === "string" ? _chunkQOAORTA4cjs.getActivityTypeDescriptor.call(void 0, type) : void 0;
279
+ if (descriptor === void 0) {
280
+ return {
281
+ status: "unscorable",
282
+ reason: `Activity type "${String(type)}" is not registered`,
283
+ maxScore: 1
284
+ };
285
+ }
286
+ if (descriptor.scoring.kind === "deferred") {
287
+ const partial = _optionalChain([descriptor, 'access', _ => _.scoring, 'access', _2 => _2.partial, 'optionalCall', _3 => _3(data, response)]);
288
+ return {
289
+ status: "deferred",
290
+ reason: descriptor.scoring.reason,
291
+ maxScore: 1,
292
+ ...partial !== void 0 ? { partial } : {}
293
+ };
294
+ }
295
+ if (isRedacted(data)) {
296
+ return {
297
+ status: "unscorable",
298
+ reason: "Activity data is redacted (no answer key), so it cannot be scored on the client. Score against the full data server-side.",
299
+ maxScore: 1
300
+ };
301
+ }
302
+ const result = descriptor.scoring.score(data, response);
303
+ if (!Number.isFinite(result.score)) {
304
+ return {
305
+ status: "unscorable",
306
+ reason: `Scoring "${descriptor.type}" produced a non-finite score; the activity data is incomplete.`,
307
+ maxScore: result.maxScore
308
+ };
309
+ }
310
+ const passed = computePassThreshold(data, result.score);
311
+ return {
312
+ status: "scored",
313
+ score: result.score,
314
+ maxScore: result.maxScore,
315
+ passed,
316
+ feedback: _nullishCoalesce(result.feedback, () => ( selectFeedback(data, passed))),
317
+ details: result.details
318
+ };
319
+ }
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+ 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;
334
+ //# sourceMappingURL=chunk-5ZAW76F3.cjs.map