@intellectif/lk-core 0.5.0 → 0.7.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.
Files changed (46) hide show
  1. package/README.md +12 -4
  2. package/dist/{activity-wkzRemHx.d.cts → activity-gelWNZ6V.d.cts} +49 -10
  3. package/dist/{activity-wkzRemHx.d.ts → activity-gelWNZ6V.d.ts} +49 -10
  4. package/dist/{chunk-WV5WQ3SZ.js → chunk-2SQ75JTE.js} +2 -2
  5. package/dist/{chunk-4JR3UXX6.cjs → chunk-5RE3ZRSA.cjs} +8 -1
  6. package/dist/chunk-5RE3ZRSA.cjs.map +1 -0
  7. package/dist/{chunk-FCM5VJBS.cjs → chunk-7NIH5IL3.cjs} +16 -13
  8. package/dist/chunk-7NIH5IL3.cjs.map +1 -0
  9. package/dist/{chunk-R7PDJBRX.cjs → chunk-AFIQQADJ.cjs} +4 -4
  10. package/dist/{chunk-R7PDJBRX.cjs.map → chunk-AFIQQADJ.cjs.map} +1 -1
  11. package/dist/{chunk-SM5HUGYU.cjs → chunk-MVZKEWZN.cjs} +21 -11
  12. package/dist/chunk-MVZKEWZN.cjs.map +1 -0
  13. package/dist/{chunk-DUQVGLQ3.js → chunk-NOBJYDOB.js} +15 -5
  14. package/dist/chunk-NOBJYDOB.js.map +1 -0
  15. package/dist/{chunk-LSHDNA2T.js → chunk-XL75ZZVM.js} +6 -3
  16. package/dist/chunk-XL75ZZVM.js.map +1 -0
  17. package/dist/{chunk-NUCEUU4P.js → chunk-YCXIHFLG.js} +8 -1
  18. package/dist/chunk-YCXIHFLG.js.map +1 -0
  19. package/dist/{index-BKyZrd94.d.cts → index-B_RLyeEj.d.ts} +101 -14
  20. package/dist/{index-CLmXzBhB.d.ts → index-CDWH2WIg.d.cts} +101 -14
  21. package/dist/index.cjs +336 -25
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +360 -6
  24. package/dist/index.d.ts +360 -6
  25. package/dist/index.js +317 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/schemas.cjs +3 -3
  28. package/dist/schemas.d.cts +2 -2
  29. package/dist/schemas.d.ts +2 -2
  30. package/dist/schemas.js +2 -2
  31. package/dist/scoring.cjs +3 -3
  32. package/dist/scoring.d.cts +2 -2
  33. package/dist/scoring.d.ts +2 -2
  34. package/dist/scoring.js +2 -2
  35. package/dist/xapi.cjs +3 -3
  36. package/dist/xapi.d.cts +1 -1
  37. package/dist/xapi.d.ts +1 -1
  38. package/dist/xapi.js +2 -2
  39. package/package.json +1 -1
  40. package/dist/chunk-4JR3UXX6.cjs.map +0 -1
  41. package/dist/chunk-DUQVGLQ3.js.map +0 -1
  42. package/dist/chunk-FCM5VJBS.cjs.map +0 -1
  43. package/dist/chunk-LSHDNA2T.js.map +0 -1
  44. package/dist/chunk-NUCEUU4P.js.map +0 -1
  45. package/dist/chunk-SM5HUGYU.cjs.map +0 -1
  46. /package/dist/{chunk-WV5WQ3SZ.js.map → chunk-2SQ75JTE.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,11 +1,351 @@
1
- import { V as ValidationError, C as CriterionScore, A as ActivityData, G as GradeRecord, m as ItemOutcome, r as ScoringResult, D as DeferredScoringPartial, F as FillInTheBlanksData, f as FillInTheBlanksLearnerResponse, M as MultipleChoiceData, o as MultipleChoiceLearnerResponse, W as WrittenResponseData, u as WrittenResponseLearnerResponse } from './activity-wkzRemHx.js';
2
- export { a as ActivityDataMap, b as ActivityFeedback, c as ActivityMedia, d as ActivityResult, e as ActivityType, B as BlankConfig, g as Grader, h as GraderKind, i as GraderUsage, j as GradingState, I as InlineCorrection, k as InteractionEvent, l as InteractionKind, L as LearnerResponse, n as LearnerResponseMap, p as MultipleChoiceOption, S as ScoringDetail, q as ScoringOutcome, T as TextMatchPolicy, s as TextMatchResult, t as ValidationResult, v as WrittenResponseRubric, w as WrittenResponseRubricCriterion, X as XAPIActor, x as XAPIConfig, y as XAPIContext, z as XAPIContextActivities, E as XAPIError, H as XAPIObject, J as XAPIResult, K as XAPIScore, N as XAPIStatement, O as XAPIVerbObject, P as levenshteinDistance, Q as matchText } from './activity-wkzRemHx.js';
3
- import { S as SequenceEntry, l as SequenceSlot, I as ItemGroup } from './index-CLmXzBhB.js';
4
- export { B as BlankConfigSchema, F as FeedbackSchema, a as FillInTheBlanksDataSchema, b as ItemGroupSchema, M as MediaSchema, c as MediaUrlSchema, d as MultipleChoiceDataSchema, e as MultipleChoiceOptionSchema, R as RedactedBlankConfigSchema, f as RedactedFillInTheBlanksDataSchema, g as RedactedItemGroupSchema, h as RedactedMultipleChoiceDataSchema, i as RedactedMultipleChoiceOptionSchema, j as RedactedStimulusSchema, k as RedactedWrittenResponseDataSchema, m as SequenceSlotGroup, n as Stimulus, o as StimulusKind, p as StimulusSchema, T as TextMatchPolicySchema, W as WrittenResponseDataSchema, q as WrittenResponseRubricCriterionSchema, r as WrittenResponseRubricSchema, s as fillInTheBlanksJsonSchema, t as itemGroupJsonSchema, u as jsonSchemaFor, v as multipleChoiceJsonSchema, w as stimulusJsonSchema, x as validateActivity, y as validateItemGroup, z as writtenResponseJsonSchema } from './index-CLmXzBhB.js';
1
+ import { ScoredItem } from './scoring.js';
2
+ export { AssessmentScore, AssessmentSectionInput, Band, CompositionPolicy, DEFAULT_PASS_THRESHOLD, PassFailureReason, RoundingMode, RoundingPolicy, SectionScore, classifyBand, composeAssessmentScore, computePassThreshold, evaluate, gte, roundGrade, score } from './scoring.js';
3
+ import { m as ItemOutcome, A as ActivityData, L as LearnerResponse, V as ValidationError, C as CriterionScore, G as GradeRecord, r as ScoringResult, D as DeferredScoringPartial, F as FillInTheBlanksData, f as FillInTheBlanksLearnerResponse, M as MultipleChoiceData, o as MultipleChoiceLearnerResponse, W as WrittenResponseData, u as WrittenResponseLearnerResponse } from './activity-gelWNZ6V.js';
4
+ export { a as ActivityDataMap, b as ActivityFeedback, c as ActivityMedia, d as ActivityResult, e as ActivityType, B as BlankConfig, g as Grader, h as GraderKind, i as GraderUsage, j as GradingState, I as InlineCorrection, k as InteractionEvent, l as InteractionKind, n as LearnerResponseMap, p as MultipleChoiceOption, S as ScoringDetail, q as ScoringOutcome, T as TextMatchPolicy, s as TextMatchResult, t as ValidationResult, v as WrittenResponseRubric, w as WrittenResponseRubricCriterion, X as XAPIActor, x as XAPIConfig, y as XAPIContext, z as XAPIContextActivities, E as XAPIError, H as XAPIObject, J as XAPIResult, K as XAPIScore, N as XAPIStatement, O as XAPIVerbObject, P as levenshteinDistance, Q as matchText } from './activity-gelWNZ6V.js';
5
+ import { s as SequenceSlot, S as SequenceEntry, I as ItemGroup } from './index-B_RLyeEj.js';
6
+ export { B as BlankConfigSchema, F as FeedbackSchema, a as FillInTheBlanksDataSchema, b as ItemGroupSchema, M as MediaSchema, c as MediaUrlSchema, d as MultipleChoiceDataSchema, e as MultipleChoiceOptionSchema, R as RedactedActivity, f as RedactedBlankConfig, g as RedactedBlankConfigSchema, h as RedactedFillInTheBlanksData, i as RedactedFillInTheBlanksDataSchema, j as RedactedItemGroupSchema, k as RedactedMultipleChoiceData, l as RedactedMultipleChoiceDataSchema, m as RedactedMultipleChoiceOption, n as RedactedMultipleChoiceOptionSchema, o as RedactedStimulus, p as RedactedStimulusSchema, q as RedactedWrittenResponseData, r as RedactedWrittenResponseDataSchema, t as SequenceSlotGroup, u as Stimulus, v as StimulusKind, w as StimulusSchema, T as TextMatchPolicySchema, W as WrittenResponseDataSchema, x as WrittenResponseRubricCriterionSchema, y as WrittenResponseRubricSchema, z as fillInTheBlanksJsonSchema, A as itemGroupJsonSchema, C as jsonSchemaFor, D as multipleChoiceJsonSchema, E as stimulusJsonSchema, G as validateActivity, H as validateItemGroup, J as writtenResponseJsonSchema } from './index-B_RLyeEj.js';
5
7
  import { z } from 'zod/v4';
6
- export { AssessmentScore, AssessmentSectionInput, Band, CompositionPolicy, DEFAULT_PASS_THRESHOLD, PassFailureReason, RoundingMode, RoundingPolicy, ScoredItem, SectionScore, classifyBand, composeAssessmentScore, computePassThreshold, evaluate, gte, roundGrade, score } from './scoring.js';
7
8
  export { AnsweredStatementParams, CompletedStatementParams, SubmittedStatementParams, XAPIObjectParams, XAPIStatementParams, XAPIVerb, XAPIVerbKey, XAPI_VERB_DISPLAY, validateXAPIStatement, xAPIBuilder, xapiDefinitionFor } from './xapi.js';
8
9
 
10
+ /**
11
+ * The frozen record of what one attempt was served.
12
+ *
13
+ * A sequence definition is live content: it gets edited, re-ordered, corrected.
14
+ * An attempt is a historical fact. Everything that decides a grade — which
15
+ * questions, in what order, each worth how much, against which version of the
16
+ * content — has to be pinned at the moment the attempt starts, or a re-grade
17
+ * six months later silently answers a different question than the learner was
18
+ * asked.
19
+ *
20
+ * This is the thing you persist next to the responses.
21
+ */
22
+ /** One planned position: an item, its identity, its worth, and its fingerprint. */
23
+ interface AttemptPlanSlot {
24
+ /**
25
+ * Slot identity, from `flattenSequence`. Stable under shuffling; stable
26
+ * under editing too when the entries declare `slotKey`. This is the key to
27
+ * store responses and scores against.
28
+ */
29
+ slotId: string;
30
+ /** 0-based PRESENTED position in this attempt. */
31
+ index: number;
32
+ /** The activity that filled the slot. */
33
+ activityId: string;
34
+ /** The activity's `type` discriminator. */
35
+ activityType: string;
36
+ /**
37
+ * What this slot is worth, frozen. Points belong to the PAPER, not the item:
38
+ * the same question can be worth 1 in a quiz and 3 in a final, so they are
39
+ * resolved when the attempt is planned and never read from content again.
40
+ */
41
+ points: number;
42
+ /**
43
+ * Fingerprint of the activity's content as served. Compare it later with
44
+ * {@link verifyAttemptPlan} to find out whether the item has been edited
45
+ * since — the difference between "re-grading this attempt" and "grading a
46
+ * different exam".
47
+ */
48
+ contentHash: string;
49
+ /** Present when the slot came from an item group. */
50
+ group?: {
51
+ id: string;
52
+ title?: string;
53
+ /** Fingerprint of the stimulus as served — a corrected passage changes the question. */
54
+ stimulusHash: string;
55
+ };
56
+ }
57
+ /** Everything needed to reproduce and re-grade one attempt. */
58
+ interface AttemptPlan {
59
+ /** Version of this envelope, so a stored plan stays readable as it evolves. */
60
+ planVersion: '1.0';
61
+ /**
62
+ * The seed every shuffle in this attempt used, when anything shuffled.
63
+ * Absent means nothing was shuffled and the order is the authored one.
64
+ */
65
+ seed?: string;
66
+ /**
67
+ * Whether the top-level entries were shuffled. Recorded because
68
+ * {@link verifyAttemptPlan} needs to rebuild the SAME presented order: a
69
+ * re-plan that omitted this returned authored order, every slot compared as
70
+ * re-ordered, and unchanged content reported as drift.
71
+ */
72
+ shuffleEntries?: boolean;
73
+ /**
74
+ * Fingerprint of the whole plan's content — every slot's identity, order,
75
+ * points and content hash. One value to store against an attempt and compare
76
+ * later; the per-slot hashes then say WHICH item moved.
77
+ */
78
+ planHash: string;
79
+ slots: AttemptPlanSlot[];
80
+ /** Sum of every slot's `points`, frozen. The denominator of the paper. */
81
+ totalPoints: number;
82
+ }
83
+ /** What {@link verifyAttemptPlan} found when a plan met current content. */
84
+ interface AttemptPlanDrift {
85
+ /** True when the plan and the content still agree in every respect. */
86
+ matches: boolean;
87
+ /** Slots in the plan that no longer exist in the content at all. */
88
+ missingSlotIds: string[];
89
+ /** Slots present now that the plan never held — content added since. */
90
+ addedSlotIds: string[];
91
+ /** Slots whose activity content has been edited since the attempt. */
92
+ changedSlotIds: string[];
93
+ /** Slots whose item group's stimulus has been edited since the attempt. */
94
+ changedStimulusSlotIds: string[];
95
+ /**
96
+ * Slots now worth a different number of points. A reweight is a paper
97
+ * change — it moves the grade without touching a single question — so it
98
+ * cannot be left out of a remark or an appeal.
99
+ */
100
+ changedPointsSlotIds: string[];
101
+ /** Slots presented at a different position now than they were then. */
102
+ reorderedSlotIds: string[];
103
+ }
104
+
105
+ /** Options for {@link planAttempt}. */
106
+ interface PlanAttemptOptions<TItem> {
107
+ /** Shuffle the top-level entries. A group moves as one block. */
108
+ shuffleEntries?: boolean;
109
+ /**
110
+ * Seed for every shuffle in this attempt. Required whenever anything
111
+ * shuffles — the attempt has to be reproducible, which is the entire point
112
+ * of writing a plan down. Use the attempt id.
113
+ */
114
+ seed?: string;
115
+ /**
116
+ * What each slot is worth. Defaults to 1 for every slot.
117
+ *
118
+ * Points are resolved HERE, not read from content, because they belong to
119
+ * the paper rather than the item: the same question is worth 1 in a practice
120
+ * quiz and 3 in a final. Whatever this returns is frozen into the plan and
121
+ * is what `composeAssessmentScore` will weight by.
122
+ *
123
+ * @throws Error when it returns a value that is not a finite, non-negative
124
+ * number — a slot worth `NaN` points would poison the whole total.
125
+ */
126
+ points?: (slot: SequenceSlot<TItem>) => number;
127
+ }
128
+ /**
129
+ * Freezes what an attempt is being served: the presented order, each slot's
130
+ * identity and worth, and a fingerprint of the content behind it.
131
+ *
132
+ * Call this once, when the attempt starts, and store the result beside the
133
+ * responses. Everything that decides the grade is then a historical fact
134
+ * rather than a re-read of content that may since have changed — which is what
135
+ * makes a re-grade, a remark or an appeal answerable.
136
+ *
137
+ * Ordering comes from `flattenSequence`, so a plan and a live render of the
138
+ * same entries with the same seed agree slot for slot.
139
+ *
140
+ * @throws Error when a shuffle is requested without a seed, when a group is
141
+ * empty, when two entries collide on a `slotKey`, or when `points`
142
+ * returns a value that is not finite and non-negative.
143
+ */
144
+ declare function planAttempt<TItem extends {
145
+ id: string;
146
+ type: string;
147
+ }>(entries: readonly SequenceEntry<TItem>[], options?: PlanAttemptOptions<TItem>): AttemptPlan;
148
+ /**
149
+ * Compares a stored plan with the content as it stands now, and reports every
150
+ * way they have drifted apart.
151
+ *
152
+ * This is the question a remark or an appeal actually asks: *is the paper I am
153
+ * looking at the paper this learner sat?* Ids alone cannot answer it, because
154
+ * they survive an edit unchanged.
155
+ *
156
+ * Rebuild the comparison plan with the SAME options the stored one recorded —
157
+ * its `seed`, its `shuffleEntries`, and the same `points` function — or the
158
+ * differences you see will be your own:
159
+ *
160
+ * ```ts
161
+ * const now = planAttempt(currentEntries, {
162
+ * seed: stored.seed,
163
+ * shuffleEntries: stored.shuffleEntries,
164
+ * points: pointsFor, // omit it and every slot reweights to 1
165
+ * });
166
+ * const drift = verifyAttemptPlan(stored, now);
167
+ * ```
168
+ *
169
+ * A drift is not automatically a problem — a fixed typo changes a hash without
170
+ * changing what was asked. It is a fact somebody has to be able to see.
171
+ */
172
+ declare function verifyAttemptPlan(plan: AttemptPlan, current: AttemptPlan): AttemptPlanDrift;
173
+ /** How {@link scoredItemsFromPlan} fills a slot that has no recorded outcome. */
174
+ type MissingOutcomePolicy =
175
+ /**
176
+ * Default. `{ status: 'deferred', reason: 'no_response_recorded' }` — a
177
+ * non-terminal state, so the composed result stays `provisional` and cannot
178
+ * be recorded as a final pass or fail.
179
+ */
180
+ 'deferred'
181
+ /**
182
+ * `{ status: 'scored', score: 0 }` — the learner left it blank on a paper
183
+ * that IS complete. Choose this only when you know the attempt was
184
+ * submitted; it is a real zero and makes the result final.
185
+ */
186
+ | 'zero'
187
+ /** Build the outcome yourself, per slot. */
188
+ | ((slot: AttemptPlanSlot) => ItemOutcome);
189
+ /**
190
+ * Turns a plan plus whatever outcomes exist into the `ScoredItem[]`
191
+ * `composeAssessmentScore` consumes.
192
+ *
193
+ * The plan is the source of the denominator, not the outcomes. A slot the
194
+ * learner never reached still has to appear — otherwise it silently leaves the
195
+ * denominator and the remaining questions quietly become worth more than the
196
+ * paper says.
197
+ *
198
+ * **A missing outcome defaults to `deferred`, never `unscorable`.** The
199
+ * distinction decides a grade: `unscorable` means "a grade is never coming",
200
+ * so `composeAssessmentScore` drops the slot from the denominator AND lets the
201
+ * result go `final` — which turned a three-question paper with one answer into
202
+ * a final, passing 100%. `deferred` means "not yet", which holds the result
203
+ * `provisional` so nothing can be recorded. Pass `'zero'` once you know the
204
+ * attempt was submitted and the blanks are genuinely blanks.
205
+ *
206
+ * Pass outcomes keyed by `slotId`. Keys the plan does not know are ignored:
207
+ * a plan is the authority on what the attempt contained.
208
+ */
209
+ declare function scoredItemsFromPlan(plan: AttemptPlan, outcomes: Readonly<Record<string, ItemOutcome>>, options?: {
210
+ missing?: MissingOutcomePolicy;
211
+ }): ScoredItem[];
212
+ /** Convenience alias: the entry type a plan is built from. */
213
+ type PlannableEntry = SequenceEntry<ActivityData>;
214
+
215
+ /**
216
+ * A resumable snapshot of an attempt in progress.
217
+ *
218
+ * The plan says what the learner was asked; this says how far they got. It is
219
+ * deliberately the smallest thing that can reopen an attempt exactly where it
220
+ * was left: the answers so far, which of them are already submitted, and where
221
+ * the learner was standing.
222
+ *
223
+ * It is bound to a plan by {@link AttemptState.planHash}, because restoring
224
+ * answers onto a DIFFERENT paper is the failure this type exists to prevent —
225
+ * slot ids alone would happily line up against the wrong questions.
226
+ */
227
+ interface AttemptState {
228
+ /** Version of this envelope, so a stored snapshot stays readable as it evolves. */
229
+ stateVersion: '1.0';
230
+ /**
231
+ * `planHash` of the {@link AttemptPlan} this state belongs to. `restoreAttemptState`
232
+ * refuses a plan that does not match, rather than restoring a learner's
233
+ * answers onto a paper they never sat.
234
+ */
235
+ planHash: string;
236
+ /** The learner's answers so far, keyed by `slotId`. */
237
+ responses: Record<string, LearnerResponse>;
238
+ /**
239
+ * Slots the learner has already submitted. Kept apart from `responses`
240
+ * because they answer different questions: what did they write, and may they
241
+ * still change it.
242
+ */
243
+ submittedSlotIds: string[];
244
+ /** Presented position the learner was on, so a resume reopens there. */
245
+ index: number;
246
+ /** ISO 8601 timestamp of the snapshot, when the caller supplies one. */
247
+ savedAt?: string;
248
+ }
249
+ /** One slot whose response differs between two snapshots. */
250
+ interface ResponseDiffEntry {
251
+ slotId: string;
252
+ /** `added` — answered since; `removed` — cleared; `changed` — a different answer. */
253
+ change: 'added' | 'removed' | 'changed';
254
+ before?: LearnerResponse;
255
+ after?: LearnerResponse;
256
+ }
257
+
258
+ /** What {@link serializeAttemptState} is given about an attempt in progress. */
259
+ interface AttemptProgress {
260
+ /** Answers so far, keyed by `slotId`. */
261
+ responses: Readonly<Record<string, LearnerResponse>>;
262
+ /** Slots already submitted. Defaults to none. */
263
+ submittedSlotIds?: readonly string[];
264
+ /** Presented position the learner is on. Defaults to 0. */
265
+ index?: number;
266
+ /** ISO 8601 timestamp to stamp the snapshot with. The SDK does not read a clock. */
267
+ savedAt?: string;
268
+ }
269
+ /**
270
+ * Captures an in-progress attempt as a storable snapshot, bound to its plan.
271
+ *
272
+ * Validated against the plan on the way in, not on the way out: a response
273
+ * stored under a slot the paper does not contain is a bug at the moment it is
274
+ * written, and finding it months later — when a learner tries to resume — is
275
+ * finding it far too late.
276
+ *
277
+ * The SDK reads no clock: pass `savedAt` if you want the snapshot stamped, so
278
+ * the function stays pure and its output stays reproducible in a test.
279
+ *
280
+ * @throws Error when a response or submitted slot is not in the plan, or when
281
+ * `index` is not a position the plan actually has.
282
+ */
283
+ declare function serializeAttemptState(plan: AttemptPlan, progress: AttemptProgress): AttemptState;
284
+ /**
285
+ * Reopens a stored snapshot against a plan, refusing anything that does not
286
+ * belong to it.
287
+ *
288
+ * The `planHash` check is the point. Slot ids are short and stable by design,
289
+ * so a snapshot from a DIFFERENT paper — last term's midterm, a sibling
290
+ * version, a copy-pasted attempt row — will happily line its answers up
291
+ * against the wrong questions and look entirely plausible doing it. Comparing
292
+ * the paper's fingerprint is what makes that impossible rather than unlikely.
293
+ *
294
+ * @throws Error when the snapshot belongs to a different plan, or references
295
+ * slots the plan does not contain.
296
+ */
297
+ declare function restoreAttemptState(plan: AttemptPlan, state: AttemptState): AttemptState;
298
+ /**
299
+ * Reports how two snapshots' responses differ, slot by slot.
300
+ *
301
+ * Useful for an autosave that should only write what moved, and for an audit
302
+ * trail that has to show what a learner changed between two saves — including
303
+ * an answer they cleared, which a naive comparison of the later snapshot
304
+ * alone cannot see.
305
+ *
306
+ * Entries come back sorted by `slotId`, so the output is stable regardless of
307
+ * the order the two objects happened to be written in.
308
+ */
309
+ declare function diffResponses(before: Pick<AttemptState, 'responses'>, after: Pick<AttemptState, 'responses'>): ResponseDiffEntry[];
310
+
311
+ /**
312
+ * Content fingerprinting — "is this the same content the learner was served?"
313
+ *
314
+ * A recorded attempt outlives the content it was taken against. Papers get
315
+ * corrected, a typo is fixed, an option is reworded — and six months later a
316
+ * remark, an appeal, or a per-item analysis is run against content that is no
317
+ * longer what the learner saw. Nothing warns anybody, because the ids all still
318
+ * match. A fingerprint stored with the attempt turns that silent drift into a
319
+ * question somebody can answer.
320
+ */
321
+ /**
322
+ * Deterministic JSON: object keys sorted, arrays left in order, `undefined`
323
+ * omitted from objects (matching `JSON.stringify`) and rendered as `null`
324
+ * inside arrays.
325
+ *
326
+ * Key order is the whole point. Two objects that differ only in the order
327
+ * their keys happened to be written are the SAME content, and a fingerprint
328
+ * that disagreed would raise a false alarm every time a payload made a
329
+ * round-trip through a different serializer.
330
+ */
331
+ declare function canonicalJson(value: unknown, seen?: Set<object>): string;
332
+ /**
333
+ * FNV-1a over the UTF-8 bytes of `input`, as 16 lowercase hex digits.
334
+ *
335
+ * **This is a change-detection fingerprint, not a tamper-evident signature.**
336
+ * It is deterministic across runtimes and dependency-free, which is what makes
337
+ * it usable in a stored grade record — but an adversary who can edit content
338
+ * can also, with effort, preserve the fingerprint. If you need the stronger
339
+ * property, sign the plan with a key the content author does not hold; this
340
+ * exists to catch honest edits, which is what actually happens.
341
+ */
342
+ declare function fingerprint(input: string): string;
343
+ /**
344
+ * Fingerprints any JSON-serialisable value through {@link canonicalJson}, so
345
+ * the result depends on the content and not on how it was written.
346
+ */
347
+ declare function contentHash(value: unknown): string;
348
+
9
349
  /**
10
350
  * Canonical word counter for written-response bounds (Req 22.9): tokens are
11
351
  * maximal runs of non-whitespace (split on `\s+`), so hyphenated forms
@@ -75,6 +415,20 @@ interface GradeFromRubricOptions {
75
415
  * marked `notApplicable`, and those carrying no numeric `score` (a purely
76
416
  * banded judgement), are excluded from both numerator and denominator. When
77
417
  * nothing scoreable remains the result is `unscorable`, never a zero.
418
+ *
419
+ * Scores need not be in [0,1]: set `maxScore` on a criterion to declare what
420
+ * its score is out of, and each is normalised before weighting. A grader
421
+ * working out of 100 says so and is done:
422
+ *
423
+ * ```ts
424
+ * gradeFromRubric([
425
+ * { name: 'Task achievement', score: 82, maxScore: 100, weight: 2 },
426
+ * { name: 'Range', score: 7, maxScore: 9, weight: 1 },
427
+ * ]);
428
+ * ```
429
+ *
430
+ * The returned `GradeRecord.score` is always scaled [0,1] against
431
+ * `maxScore: 1`, like every other score in the SDK.
78
432
  */
79
433
  declare function gradeFromRubric(criteria: readonly CriterionScore[], activityData?: ActivityData, options?: GradeFromRubricOptions): GradeRecord | {
80
434
  unscorable: true;
@@ -475,4 +829,4 @@ interface ThemeTokens {
475
829
  '--lk-transition-base': string;
476
830
  }
477
831
 
478
- export { ActivityData, ActivitySchemaError, type ActivityTypeDescriptor, type ActivityTypeInterop, type ActivityTypeScoring, CriterionScore, DeferredScoringError, DeferredScoringPartial, type FieldPolicy, FillInTheBlanksData, FillInTheBlanksLearnerResponse, type FlattenSequenceOptions, type GradeFromRubricOptions, GradeRecord, ItemGroup, ItemOutcome, MultipleChoiceData, MultipleChoiceLearnerResponse, type PartialScoringResult, type RedactOptions, type RedactedActivityData, type RedactedItemGroup, RedactedScoringError, type RegisteredActivityTypeDescriptor, ScoringResult, type SeededShuffleOptions, type Sensitivity, SequenceEntry, SequenceSlot, type ShuffleVersion, type ThemeTokens, UnknownActivityTypeError, ValidationError, WrittenResponseData, WrittenResponseLearnerResponse, type XAPIInteractionType, assertRedacted, assertRedactedItemGroup, countWords, defineActivityType, fillInTheBlanksType, flattenSequence, getActivityTypeDescriptor, gradeFromRubric, hasGrade, hashSeed, isItemGroup, multipleChoiceType, outcomeFromGrade, redact, redactItemGroup, registerActivityType, registeredActivityTypes, seededShuffle, writtenResponseType };
832
+ export { ActivityData, ActivitySchemaError, type ActivityTypeDescriptor, type ActivityTypeInterop, type ActivityTypeScoring, type AttemptPlan, type AttemptPlanDrift, type AttemptPlanSlot, type AttemptProgress, type AttemptState, CriterionScore, DeferredScoringError, DeferredScoringPartial, type FieldPolicy, FillInTheBlanksData, FillInTheBlanksLearnerResponse, type FlattenSequenceOptions, type GradeFromRubricOptions, GradeRecord, ItemGroup, ItemOutcome, LearnerResponse, type MissingOutcomePolicy, MultipleChoiceData, MultipleChoiceLearnerResponse, type PartialScoringResult, type PlanAttemptOptions, type PlannableEntry, type RedactOptions, type RedactedActivityData, type RedactedItemGroup, RedactedScoringError, type RegisteredActivityTypeDescriptor, type ResponseDiffEntry, ScoredItem, ScoringResult, type SeededShuffleOptions, type Sensitivity, SequenceEntry, SequenceSlot, type ShuffleVersion, type ThemeTokens, UnknownActivityTypeError, ValidationError, WrittenResponseData, WrittenResponseLearnerResponse, type XAPIInteractionType, assertRedacted, assertRedactedItemGroup, canonicalJson, contentHash, countWords, defineActivityType, diffResponses, fillInTheBlanksType, fingerprint, flattenSequence, getActivityTypeDescriptor, gradeFromRubric, hasGrade, hashSeed, isItemGroup, multipleChoiceType, outcomeFromGrade, planAttempt, redact, redactItemGroup, registerActivityType, registeredActivityTypes, restoreAttemptState, scoredItemsFromPlan, seededShuffle, serializeAttemptState, verifyAttemptPlan, writtenResponseType };