@intellectif/lk-core 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +2 -0
- package/dist/{activity-DARHcrz_.d.cts → activity-CPtJUBek.d.cts} +65 -2
- package/dist/{activity-DARHcrz_.d.ts → activity-CPtJUBek.d.ts} +65 -2
- package/dist/{chunk-XL75ZZVM.js → chunk-2M76F32Y.js} +10 -6
- package/dist/chunk-2M76F32Y.js.map +1 -0
- package/dist/{chunk-AFIQQADJ.cjs → chunk-7ACNBDSF.cjs} +4 -4
- package/dist/{chunk-AFIQQADJ.cjs.map → chunk-7ACNBDSF.cjs.map} +1 -1
- package/dist/{chunk-5RE3ZRSA.cjs → chunk-FKH4YT5X.cjs} +134 -18
- package/dist/chunk-FKH4YT5X.cjs.map +1 -0
- package/dist/{chunk-7NIH5IL3.cjs → chunk-FVLKIL6W.cjs} +19 -15
- package/dist/chunk-FVLKIL6W.cjs.map +1 -0
- package/dist/{chunk-YCXIHFLG.js → chunk-HCOWSGEZ.js} +127 -11
- package/dist/chunk-HCOWSGEZ.js.map +1 -0
- package/dist/{chunk-NOBJYDOB.js → chunk-LNA33IK7.js} +2 -2
- package/dist/{chunk-2SQ75JTE.js → chunk-NHGXOOZ2.js} +2 -2
- package/dist/{chunk-MVZKEWZN.cjs → chunk-YJZY5TPY.cjs} +8 -8
- package/dist/{chunk-MVZKEWZN.cjs.map → chunk-YJZY5TPY.cjs.map} +1 -1
- package/dist/{index-BfLwCMXN.d.cts → index-BvrA8nIV.d.ts} +353 -9
- package/dist/{index-yrUw7NzZ.d.ts → index-gIN564mV.d.cts} +353 -9
- package/dist/index.cjs +177 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -5
- package/dist/index.d.ts +173 -5
- package/dist/index.js +157 -7
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +9 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +8 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +2 -2
- package/package.json +2 -1
- package/dist/chunk-5RE3ZRSA.cjs.map +0 -1
- package/dist/chunk-7NIH5IL3.cjs.map +0 -1
- package/dist/chunk-XL75ZZVM.js.map +0 -1
- package/dist/chunk-YCXIHFLG.js.map +0 -1
- /package/dist/{chunk-NOBJYDOB.js.map → chunk-LNA33IK7.js.map} +0 -0
- /package/dist/{chunk-2SQ75JTE.js.map → chunk-NHGXOOZ2.js.map} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ScoredItem } from './scoring.js';
|
|
2
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-
|
|
4
|
-
export { a as ActivityDataMap, b as ActivityFeedback,
|
|
5
|
-
import {
|
|
6
|
-
export { B as BlankConfigSchema, F as FeedbackSchema, a as FillInTheBlanksDataSchema, b as ItemGroupSchema, M as
|
|
3
|
+
import { m as ItemOutcome, A as ActivityData, L as LearnerResponse, V as ValidationError, C as CriterionScore, G as GradeRecord, N as NativeControlHint, c as ActivityMedia, 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-CPtJUBek.js';
|
|
4
|
+
export { a as ActivityDataMap, b as ActivityFeedback, 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, O as XAPIStatement, P as XAPIVerbObject, Q as levenshteinDistance, R as matchText } from './activity-CPtJUBek.js';
|
|
5
|
+
import { u as SequenceSlot, S as SequenceEntry, I as ItemGroup } from './index-BvrA8nIV.js';
|
|
6
|
+
export { B as BlankConfigSchema, F as FeedbackSchema, a as FillInTheBlanksDataSchema, b as ItemGroupSchema, M as MediaPlaybackSchema, c as MediaSchema, d as MediaUrlSchema, e as MultipleChoiceDataSchema, f as MultipleChoiceOptionSchema, N as NativeControlHintSchema, R as RedactedActivity, g as RedactedBlankConfig, h as RedactedBlankConfigSchema, i as RedactedFillInTheBlanksData, j as RedactedFillInTheBlanksDataSchema, k as RedactedItemGroupSchema, l as RedactedMediaSchema, m as RedactedMultipleChoiceData, n as RedactedMultipleChoiceDataSchema, o as RedactedMultipleChoiceOption, p as RedactedMultipleChoiceOptionSchema, q as RedactedStimulus, r as RedactedStimulusSchema, s as RedactedWrittenResponseData, t as RedactedWrittenResponseDataSchema, v as SequenceSlotGroup, w as Stimulus, x as StimulusKind, y as StimulusSchema, T as TextMatchPolicySchema, W as WrittenResponseDataSchema, z as WrittenResponseRubricCriterionSchema, A as WrittenResponseRubricSchema, C as fillInTheBlanksJsonSchema, D as itemGroupJsonSchema, E as jsonSchemaFor, G as multipleChoiceJsonSchema, H as stimulusJsonSchema, J as validateActivity, K as validateItemGroup, L as writtenResponseJsonSchema } from './index-BvrA8nIV.js';
|
|
7
7
|
import { z } from 'zod/v4';
|
|
8
8
|
export { AnsweredStatementParams, CompletedStatementParams, SubmittedStatementParams, XAPIObjectParams, XAPIStatementParams, XAPIVerb, XAPIVerbKey, XAPI_VERB_DISPLAY, validateXAPIStatement, xAPIBuilder, xapiDefinitionFor } from './xapi.js';
|
|
9
9
|
|
|
@@ -53,6 +53,25 @@ interface AttemptPlanSlot {
|
|
|
53
53
|
/** Fingerprint of the stimulus as served — a corrected passage changes the question. */
|
|
54
54
|
stimulusHash: string;
|
|
55
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Play budgets in force for this slot when the attempt was planned.
|
|
58
|
+
*
|
|
59
|
+
* Frozen for the same reason `points` is: a budget decides the grade, so an
|
|
60
|
+
* author who edits `maxPlays: 2 → 4` mid-window must not be able to change
|
|
61
|
+
* what a past learner was held to. A group's stimulus budget repeats on every
|
|
62
|
+
* slot of the group, exactly as `group.stimulusHash` already does.
|
|
63
|
+
*
|
|
64
|
+
* Absent when the slot budgets nothing, which keeps the plan — and therefore
|
|
65
|
+
* `planHash` — byte-identical for every paper written before 0.8.0.
|
|
66
|
+
*/
|
|
67
|
+
mediaBudgets?: MediaBudgetRef[];
|
|
68
|
+
}
|
|
69
|
+
/** A budgeted recording a slot presents, frozen as served. */
|
|
70
|
+
interface MediaBudgetRef {
|
|
71
|
+
/** `slot:<slotId>` or `stimulus:<entryKey>`. */
|
|
72
|
+
key: string;
|
|
73
|
+
/** The `maxPlays` in force when the attempt was planned. */
|
|
74
|
+
maxPlays: number;
|
|
56
75
|
}
|
|
57
76
|
/** Everything needed to reproduce and re-grade one attempt. */
|
|
58
77
|
interface AttemptPlan {
|
|
@@ -490,6 +509,155 @@ declare function flattenSequence<TItem extends {
|
|
|
490
509
|
type: string;
|
|
491
510
|
}>(entries: readonly SequenceEntry<TItem>[], options?: FlattenSequenceOptions): SequenceSlot<TItem>[];
|
|
492
511
|
|
|
512
|
+
/**
|
|
513
|
+
* What one budgeted recording has cost the learner so far.
|
|
514
|
+
*/
|
|
515
|
+
interface MediaPlayLedgerEntry {
|
|
516
|
+
/**
|
|
517
|
+
* Plays consumed. A non-negative integer. It MAY exceed the budget: an
|
|
518
|
+
* invigilator override and a race the server resolved both legitimately
|
|
519
|
+
* produce that, and clamping here would hide it.
|
|
520
|
+
*/
|
|
521
|
+
plays: number;
|
|
522
|
+
/**
|
|
523
|
+
* Where playback stood at the last report, in seconds.
|
|
524
|
+
*
|
|
525
|
+
* Restored on mount so a refresh RESUMES the play the learner already paid
|
|
526
|
+
* for instead of charging them again for it. It can only ever let them
|
|
527
|
+
* continue from where they were, never restart, because `seek` resolves to
|
|
528
|
+
* `'none'` under a budget.
|
|
529
|
+
*/
|
|
530
|
+
at?: number;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The spent-play ledger for one attempt.
|
|
534
|
+
*
|
|
535
|
+
* Deliberately **not** part of `AttemptState`, for two reasons that both bite
|
|
536
|
+
* in production:
|
|
537
|
+
*
|
|
538
|
+
* 1. It is written on a different cadence. A play is charged *immediately*,
|
|
539
|
+
* before audio is audible; answers are autosaved on a debounce. Batching a
|
|
540
|
+
* payment with a debounced write is how a learner starts a third play and
|
|
541
|
+
* hard-reloads inside the debounce window.
|
|
542
|
+
* 2. `restoreAttemptState` rebuilds its output field by field. A build that
|
|
543
|
+
* predates this release would not merely fail to read a `mediaPlays` field —
|
|
544
|
+
* it would write the snapshot back **without** it, erasing spent plays
|
|
545
|
+
* across a staged rollout, at exactly the moment they matter.
|
|
546
|
+
*
|
|
547
|
+
* Bound to its paper by `planHash` for the same reason `AttemptState` is.
|
|
548
|
+
*/
|
|
549
|
+
interface MediaPlayLedger {
|
|
550
|
+
ledgerVersion: '1.0';
|
|
551
|
+
/** The `planHash` of the paper these counts were spent on. */
|
|
552
|
+
planHash: string;
|
|
553
|
+
/** Keyed by `slotMediaKey()` / `stimulusMediaKey()`. An absent key means nothing spent. */
|
|
554
|
+
entries: Record<string, MediaPlayLedgerEntry>;
|
|
555
|
+
/** ISO 8601, when the caller supplies one. The SDK reads no clock. */
|
|
556
|
+
savedAt?: string;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* What the SDK asks the consumer to record, before a sample of audio is
|
|
560
|
+
* audible.
|
|
561
|
+
*
|
|
562
|
+
* `previousPlaysUsed` is what makes a compare-and-set possible: two tabs both
|
|
563
|
+
* seeded at 0 both claim 1, and only an atomic write can tell them apart.
|
|
564
|
+
*/
|
|
565
|
+
interface MediaPlayClaim {
|
|
566
|
+
/** `slot:…` / `stimulus:…`. */
|
|
567
|
+
key: string;
|
|
568
|
+
/** The count the client believes was already spent — the value to compare against. */
|
|
569
|
+
previousPlaysUsed: number;
|
|
570
|
+
/** The count the client is claiming: `previousPlaysUsed + 1`. */
|
|
571
|
+
playsUsed: number;
|
|
572
|
+
maxPlays: number;
|
|
573
|
+
playsRemaining: number;
|
|
574
|
+
/** Where the learner stood when the claim was made. Forensics; never identity. */
|
|
575
|
+
slotId: string;
|
|
576
|
+
index: number;
|
|
577
|
+
activityId?: string;
|
|
578
|
+
}
|
|
579
|
+
/** The consumer's authoritative answer to a {@link MediaPlayClaim}. */
|
|
580
|
+
interface MediaPlayGrant {
|
|
581
|
+
/**
|
|
582
|
+
* The count AFTER the server's atomic write. A value greater than
|
|
583
|
+
* `maxPlays` refuses the play — which is how a second tab is caught.
|
|
584
|
+
*/
|
|
585
|
+
playsUsed: number;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The authored-entry prefix of a slot id: `"3"` for both `"3"` and `"3.1"`.
|
|
590
|
+
*
|
|
591
|
+
* Safe because every slot id in a plan is produced by `flattenSequence`, and
|
|
592
|
+
* `keyOf` rejects a `.` in both an entry key and an item key — so the first
|
|
593
|
+
* `.` is always the group/item separator.
|
|
594
|
+
*/
|
|
595
|
+
declare function entryKeyOf(slotId: string): string;
|
|
596
|
+
/** Budget key for an activity's OWN `data.media`. One budget per slot. */
|
|
597
|
+
declare function slotMediaKey(slotId: string): string;
|
|
598
|
+
/**
|
|
599
|
+
* Budget key for an item group's `stimulus.media`.
|
|
600
|
+
*
|
|
601
|
+
* Keyed by the ENTRY, not the slot: one recording serves every question in the
|
|
602
|
+
* group, so `"3.0"`…`"3.5"` share one budget. Keying by `slotId` would hand a
|
|
603
|
+
* six-question `maxPlays: 2` listening group twelve plays; keying by
|
|
604
|
+
* `stimulus.id` would collide across papers, which is the thing slot keys and
|
|
605
|
+
* `planHash` exist to prevent.
|
|
606
|
+
*/
|
|
607
|
+
declare function stimulusMediaKey(slotId: string): string;
|
|
608
|
+
/** A playback policy with every default resolved. One derivation, used everywhere. */
|
|
609
|
+
interface ResolvedPlaybackPolicy {
|
|
610
|
+
controls: 'native' | 'minimal';
|
|
611
|
+
/** `null` when the recording is unbudgeted. */
|
|
612
|
+
maxPlays: number | null;
|
|
613
|
+
seek: 'allow' | 'none';
|
|
614
|
+
rate: 'allow' | 'fixed';
|
|
615
|
+
nativeControlHints: readonly NativeControlHint[];
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Resolves the defaults once, so the schema refinements, the plan and the
|
|
619
|
+
* renderer cannot drift apart about what a policy means.
|
|
620
|
+
*
|
|
621
|
+
* Absent `playback` resolves to exactly the SDK's pre-0.8.0 behaviour:
|
|
622
|
+
* `controls: 'native'`, no budget, free seeking, free speed, no hints.
|
|
623
|
+
*/
|
|
624
|
+
declare function resolvePlaybackPolicy(media: ActivityMedia): ResolvedPlaybackPolicy;
|
|
625
|
+
/**
|
|
626
|
+
* Every budgeted recording a plan contains: budget key → the `maxPlays` that
|
|
627
|
+
* was frozen when the attempt was planned.
|
|
628
|
+
*
|
|
629
|
+
* Use it to build the storage rows an attempt needs, and to check a stored
|
|
630
|
+
* ledger against the paper it claims to belong to.
|
|
631
|
+
*/
|
|
632
|
+
declare function planMediaBudgets(plan: AttemptPlan): Record<string, number>;
|
|
633
|
+
/**
|
|
634
|
+
* Validates a set of spent-play counts against the plan that budgeted them,
|
|
635
|
+
* and stamps the envelope.
|
|
636
|
+
*
|
|
637
|
+
* Validates on the way **in**: a play recorded against a recording the paper
|
|
638
|
+
* does not budget is a bug at the moment it is written, and discovering it
|
|
639
|
+
* when a learner tries to resume is discovering it far too late.
|
|
640
|
+
*
|
|
641
|
+
* @throws Error when a key is not budgeted by the plan, or a count is not a
|
|
642
|
+
* non-negative integer, or a position is not a finite number.
|
|
643
|
+
*/
|
|
644
|
+
declare function serializeMediaPlayLedger(plan: AttemptPlan, entries: Readonly<Record<string, MediaPlayLedgerEntry>>, options?: {
|
|
645
|
+
savedAt?: string;
|
|
646
|
+
}): MediaPlayLedger;
|
|
647
|
+
/**
|
|
648
|
+
* Reopens a stored ledger against the paper it was spent on.
|
|
649
|
+
*
|
|
650
|
+
* Refuses a ledger from a different paper for the same reason
|
|
651
|
+
* `restoreAttemptState` does: budget keys are short and repeat across papers
|
|
652
|
+
* (`slot:0`, `stimulus:1`), so a ledger from last term's midterm would hold a
|
|
653
|
+
* learner to a budget from an exam they never sat, and look entirely plausible
|
|
654
|
+
* doing it.
|
|
655
|
+
*
|
|
656
|
+
* @throws Error on an unrecognised `ledgerVersion`, a `planHash` mismatch, or
|
|
657
|
+
* anything `serializeMediaPlayLedger` refuses.
|
|
658
|
+
*/
|
|
659
|
+
declare function restoreMediaPlayLedger(plan: AttemptPlan, stored: MediaPlayLedger): MediaPlayLedger;
|
|
660
|
+
|
|
493
661
|
/** ScoringResult without `passed` — the public `score()` / `evaluate()` fill that in. */
|
|
494
662
|
type PartialScoringResult = Omit<ScoringResult, 'passed'>;
|
|
495
663
|
/**
|
|
@@ -829,4 +997,4 @@ interface ThemeTokens {
|
|
|
829
997
|
'--lk-transition-base': string;
|
|
830
998
|
}
|
|
831
999
|
|
|
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 };
|
|
1000
|
+
export { ActivityData, ActivityMedia, 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 MediaBudgetRef, type MediaPlayClaim, type MediaPlayGrant, type MediaPlayLedger, type MediaPlayLedgerEntry, type MissingOutcomePolicy, MultipleChoiceData, MultipleChoiceLearnerResponse, type PartialScoringResult, type PlanAttemptOptions, type PlannableEntry, type RedactOptions, type RedactedActivityData, type RedactedItemGroup, RedactedScoringError, type RegisteredActivityTypeDescriptor, type ResolvedPlaybackPolicy, 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, entryKeyOf, fillInTheBlanksType, fingerprint, flattenSequence, getActivityTypeDescriptor, gradeFromRubric, hasGrade, hashSeed, isItemGroup, multipleChoiceType, outcomeFromGrade, planAttempt, planMediaBudgets, redact, redactItemGroup, registerActivityType, registeredActivityTypes, resolvePlaybackPolicy, restoreAttemptState, restoreMediaPlayLedger, scoredItemsFromPlan, seededShuffle, serializeAttemptState, serializeMediaPlayLedger, slotMediaKey, stimulusMediaKey, verifyAttemptPlan, writtenResponseType };
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
outcomeFromGrade,
|
|
11
11
|
roundGrade,
|
|
12
12
|
score
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-LNA33IK7.js";
|
|
14
14
|
import {
|
|
15
15
|
XAPIVerb,
|
|
16
16
|
XAPI_VERB_DISPLAY,
|
|
17
17
|
validateXAPIStatement,
|
|
18
18
|
xAPIBuilder,
|
|
19
19
|
xapiDefinitionFor
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-NHGXOOZ2.js";
|
|
21
21
|
import {
|
|
22
22
|
ItemGroupSchema,
|
|
23
23
|
RedactedItemGroupSchema,
|
|
@@ -31,19 +31,23 @@ import {
|
|
|
31
31
|
validateActivity,
|
|
32
32
|
validateItemGroup,
|
|
33
33
|
writtenResponseJsonSchema
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-2M76F32Y.js";
|
|
35
35
|
import {
|
|
36
36
|
ActivitySchemaError,
|
|
37
37
|
BlankConfigSchema,
|
|
38
38
|
DeferredScoringError,
|
|
39
39
|
FeedbackSchema,
|
|
40
40
|
FillInTheBlanksDataSchema,
|
|
41
|
+
MEDIA_FIELD_POLICY,
|
|
42
|
+
MediaPlaybackSchema,
|
|
41
43
|
MediaSchema,
|
|
42
44
|
MediaUrlSchema,
|
|
43
45
|
MultipleChoiceDataSchema,
|
|
44
46
|
MultipleChoiceOptionSchema,
|
|
47
|
+
NativeControlHintSchema,
|
|
45
48
|
RedactedBlankConfigSchema,
|
|
46
49
|
RedactedFillInTheBlanksDataSchema,
|
|
50
|
+
RedactedMediaSchema,
|
|
47
51
|
RedactedMultipleChoiceDataSchema,
|
|
48
52
|
RedactedMultipleChoiceOptionSchema,
|
|
49
53
|
RedactedScoringError,
|
|
@@ -63,7 +67,7 @@ import {
|
|
|
63
67
|
registerActivityType,
|
|
64
68
|
registeredActivityTypes,
|
|
65
69
|
writtenResponseType
|
|
66
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-HCOWSGEZ.js";
|
|
67
71
|
|
|
68
72
|
// src/content-hash.ts
|
|
69
73
|
function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
@@ -228,6 +232,99 @@ function flattenSequence(entries, options = {}) {
|
|
|
228
232
|
return slots;
|
|
229
233
|
}
|
|
230
234
|
|
|
235
|
+
// src/media-budget.ts
|
|
236
|
+
function entryKeyOf(slotId) {
|
|
237
|
+
const dot = slotId.indexOf(".");
|
|
238
|
+
return dot === -1 ? slotId : slotId.slice(0, dot);
|
|
239
|
+
}
|
|
240
|
+
function slotMediaKey(slotId) {
|
|
241
|
+
return `slot:${slotId}`;
|
|
242
|
+
}
|
|
243
|
+
function stimulusMediaKey(slotId) {
|
|
244
|
+
return `stimulus:${entryKeyOf(slotId)}`;
|
|
245
|
+
}
|
|
246
|
+
function resolvePlaybackPolicy(media) {
|
|
247
|
+
const p = media.playback;
|
|
248
|
+
const budgeted = typeof p?.maxPlays === "number";
|
|
249
|
+
const seek = p?.seek ?? (budgeted ? "none" : "allow");
|
|
250
|
+
const rate = p?.rate ?? "allow";
|
|
251
|
+
const controls = p?.controls ?? (budgeted || seek === "none" || rate === "fixed" ? "minimal" : "native");
|
|
252
|
+
return {
|
|
253
|
+
controls,
|
|
254
|
+
maxPlays: budgeted ? p?.maxPlays : null,
|
|
255
|
+
seek,
|
|
256
|
+
rate,
|
|
257
|
+
nativeControlHints: p?.nativeControlHints ?? []
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function planMediaBudgets(plan) {
|
|
261
|
+
const out = {};
|
|
262
|
+
for (const slot of plan.slots) {
|
|
263
|
+
for (const budget of slot.mediaBudgets ?? []) {
|
|
264
|
+
out[budget.key] = budget.maxPlays;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function assertEntry(key, entry) {
|
|
270
|
+
if (entry === null || typeof entry !== "object" || !Number.isInteger(entry.plays) || entry.plays < 0) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`serializeMediaPlayLedger: play count for ${JSON.stringify(key)} is ${JSON.stringify(entry?.plays)}. A count must be a non-negative integer; Number(a NULL column) is the usual cause.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (entry.at !== void 0 && (!Number.isFinite(entry.at) || entry.at < 0)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`serializeMediaPlayLedger: position for ${JSON.stringify(key)} is ${JSON.stringify(entry.at)}. A position must be a finite number of seconds >= 0.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function serializeMediaPlayLedger(plan, entries, options = {}) {
|
|
282
|
+
if (entries === null || typeof entries !== "object") {
|
|
283
|
+
throw new Error("serializeMediaPlayLedger: entries must be an object keyed by media key.");
|
|
284
|
+
}
|
|
285
|
+
const budgets = planMediaBudgets(plan);
|
|
286
|
+
const out = {};
|
|
287
|
+
for (const key of Object.keys(entries)) {
|
|
288
|
+
if (!Object.hasOwn(budgets, key)) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`serializeMediaPlayLedger: media play recorded against a recording the plan does not budget: ${JSON.stringify(key)}. Keys come from slotMediaKey() / stimulusMediaKey(); a key that belongs to no budgeted media means the client and the plan disagree about which paper this is.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const entry = entries[key];
|
|
294
|
+
assertEntry(key, entry);
|
|
295
|
+
out[key] = { plays: entry.plays, ...entry.at !== void 0 ? { at: entry.at } : {} };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
ledgerVersion: "1.0",
|
|
299
|
+
planHash: plan.planHash,
|
|
300
|
+
entries: out,
|
|
301
|
+
...options.savedAt !== void 0 ? { savedAt: options.savedAt } : {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function restoreMediaPlayLedger(plan, stored) {
|
|
305
|
+
if (stored === null || typeof stored !== "object") {
|
|
306
|
+
throw new Error("restoreMediaPlayLedger: the stored ledger is not an object.");
|
|
307
|
+
}
|
|
308
|
+
if (stored.ledgerVersion !== "1.0") {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`restoreMediaPlayLedger: unsupported ledgerVersion ${JSON.stringify(stored.ledgerVersion)}. This build understands "1.0"; a newer ledger must be migrated before it is restored.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (stored.entries === null || typeof stored.entries !== "object") {
|
|
314
|
+
throw new Error(
|
|
315
|
+
"restoreMediaPlayLedger: the ledger carries no entries object (a NULL column, or a partially written row)."
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (stored.planHash !== plan.planHash) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`restoreMediaPlayLedger: this ledger belongs to a different paper (ledger ${stored.planHash}, plan ${plan.planHash}). Restoring it would hold the learner to a play budget from an exam they never sat.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return serializeMediaPlayLedger(plan, stored.entries, {
|
|
324
|
+
...stored.savedAt !== void 0 ? { savedAt: stored.savedAt } : {}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
231
328
|
// src/attempt-plan.ts
|
|
232
329
|
function contentOf(activity) {
|
|
233
330
|
if (!Object.hasOwn(activity, "slotKey")) {
|
|
@@ -236,12 +333,50 @@ function contentOf(activity) {
|
|
|
236
333
|
const { slotKey: _slotKey, ...content } = activity;
|
|
237
334
|
return content;
|
|
238
335
|
}
|
|
336
|
+
function mediaBudgetsOf(slot) {
|
|
337
|
+
const budgets = [];
|
|
338
|
+
const own = slot.activity.media;
|
|
339
|
+
if (own !== void 0) {
|
|
340
|
+
const maxPlays = resolvePlaybackPolicy(own).maxPlays;
|
|
341
|
+
if (maxPlays !== null) {
|
|
342
|
+
budgets.push({ key: slotMediaKey(slot.slotId), maxPlays });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const stimulusMedia = slot.group?.stimulus.media;
|
|
346
|
+
if (stimulusMedia !== void 0) {
|
|
347
|
+
const maxPlays = resolvePlaybackPolicy(stimulusMedia).maxPlays;
|
|
348
|
+
if (maxPlays !== null) {
|
|
349
|
+
budgets.push({ key: stimulusMediaKey(slot.slotId), maxPlays });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return budgets.length > 0 ? budgets : void 0;
|
|
353
|
+
}
|
|
354
|
+
function assertNoSplitBudgets(slots) {
|
|
355
|
+
const keysByUrl = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const slot of slots) {
|
|
357
|
+
const own = slot.activity.media;
|
|
358
|
+
if (own === void 0 || resolvePlaybackPolicy(own).maxPlays === null) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const keys = keysByUrl.get(own.url) ?? [];
|
|
362
|
+
keys.push(slotMediaKey(slot.slotId));
|
|
363
|
+
keysByUrl.set(own.url, keys);
|
|
364
|
+
}
|
|
365
|
+
for (const [url, keys] of keysByUrl) {
|
|
366
|
+
if (keys.length > 1) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`planAttempt: media ${JSON.stringify(url)} is budgeted under ${keys.length} separate keys (${keys.join(", ")}), so one recording grants ${keys.length} \xD7 maxPlays. Put the questions that share a recording in an item group \u2014 a group's stimulus is one recording with one budget.`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
239
373
|
function planSlot(slot, points) {
|
|
240
374
|
if (!Number.isFinite(points) || points < 0) {
|
|
241
375
|
throw new Error(
|
|
242
376
|
`planAttempt: slot "${slot.slotId}" resolved to ${String(points)} points. Points must be a finite, non-negative number, or the paper has no defensible total.`
|
|
243
377
|
);
|
|
244
378
|
}
|
|
379
|
+
const mediaBudgets = mediaBudgetsOf(slot);
|
|
245
380
|
return {
|
|
246
381
|
slotId: slot.slotId,
|
|
247
382
|
index: slot.index,
|
|
@@ -255,7 +390,8 @@ function planSlot(slot, points) {
|
|
|
255
390
|
...slot.group.title !== void 0 ? { title: slot.group.title } : {},
|
|
256
391
|
stimulusHash: contentHash(slot.group.stimulus)
|
|
257
392
|
}
|
|
258
|
-
} : {}
|
|
393
|
+
} : {},
|
|
394
|
+
...mediaBudgets !== void 0 ? { mediaBudgets } : {}
|
|
259
395
|
};
|
|
260
396
|
}
|
|
261
397
|
function planAttempt(entries, options = {}) {
|
|
@@ -264,6 +400,7 @@ function planAttempt(entries, options = {}) {
|
|
|
264
400
|
...shuffleEntries !== void 0 ? { shuffleEntries } : {},
|
|
265
401
|
...seed !== void 0 ? { seed } : {}
|
|
266
402
|
});
|
|
403
|
+
assertNoSplitBudgets(slots);
|
|
267
404
|
const planned = slots.map((slot) => planSlot(slot, points?.(slot) ?? 1));
|
|
268
405
|
const totalPoints = planned.reduce((sum, slot) => sum + slot.points, 0);
|
|
269
406
|
return {
|
|
@@ -455,6 +592,9 @@ function keepField(sensitivity, reveal) {
|
|
|
455
592
|
}
|
|
456
593
|
return sensitivity === "answer-key" && reveal === "after-submit";
|
|
457
594
|
}
|
|
595
|
+
function isEmptyObject(value) {
|
|
596
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
597
|
+
}
|
|
458
598
|
function redactValue(value, policy, reveal) {
|
|
459
599
|
if (Array.isArray(value)) {
|
|
460
600
|
return value.map((element) => redactValue(element, policy, reveal));
|
|
@@ -479,7 +619,7 @@ function redactValue(value, policy, reveal) {
|
|
|
479
619
|
continue;
|
|
480
620
|
}
|
|
481
621
|
const nested = redactValue(fieldValue, classification, reveal);
|
|
482
|
-
if (nested !== void 0) {
|
|
622
|
+
if (nested !== void 0 && !isEmptyObject(nested)) {
|
|
483
623
|
output[key] = nested;
|
|
484
624
|
}
|
|
485
625
|
}
|
|
@@ -562,7 +702,7 @@ var STIMULUS_FIELD_POLICY = {
|
|
|
562
702
|
title: "public",
|
|
563
703
|
body: "public",
|
|
564
704
|
bodyHtml: "public",
|
|
565
|
-
media:
|
|
705
|
+
media: MEDIA_FIELD_POLICY,
|
|
566
706
|
locale: "public",
|
|
567
707
|
attribution: "public",
|
|
568
708
|
transcript: "author-only"
|
|
@@ -654,13 +794,16 @@ export {
|
|
|
654
794
|
FeedbackSchema,
|
|
655
795
|
FillInTheBlanksDataSchema,
|
|
656
796
|
ItemGroupSchema,
|
|
797
|
+
MediaPlaybackSchema,
|
|
657
798
|
MediaSchema,
|
|
658
799
|
MediaUrlSchema,
|
|
659
800
|
MultipleChoiceDataSchema,
|
|
660
801
|
MultipleChoiceOptionSchema,
|
|
802
|
+
NativeControlHintSchema,
|
|
661
803
|
RedactedBlankConfigSchema,
|
|
662
804
|
RedactedFillInTheBlanksDataSchema,
|
|
663
805
|
RedactedItemGroupSchema,
|
|
806
|
+
RedactedMediaSchema,
|
|
664
807
|
RedactedMultipleChoiceDataSchema,
|
|
665
808
|
RedactedMultipleChoiceOptionSchema,
|
|
666
809
|
RedactedScoringError,
|
|
@@ -684,6 +827,7 @@ export {
|
|
|
684
827
|
countWords,
|
|
685
828
|
defineActivityType,
|
|
686
829
|
diffResponses,
|
|
830
|
+
entryKeyOf,
|
|
687
831
|
evaluate,
|
|
688
832
|
fillInTheBlanksJsonSchema,
|
|
689
833
|
fillInTheBlanksType,
|
|
@@ -703,17 +847,23 @@ export {
|
|
|
703
847
|
multipleChoiceType,
|
|
704
848
|
outcomeFromGrade,
|
|
705
849
|
planAttempt,
|
|
850
|
+
planMediaBudgets,
|
|
706
851
|
redact,
|
|
707
852
|
redactItemGroup,
|
|
708
853
|
registerActivityType,
|
|
709
854
|
registeredActivityTypes,
|
|
855
|
+
resolvePlaybackPolicy,
|
|
710
856
|
restoreAttemptState,
|
|
857
|
+
restoreMediaPlayLedger,
|
|
711
858
|
roundGrade,
|
|
712
859
|
score,
|
|
713
860
|
scoredItemsFromPlan,
|
|
714
861
|
seededShuffle,
|
|
715
862
|
serializeAttemptState,
|
|
863
|
+
serializeMediaPlayLedger,
|
|
864
|
+
slotMediaKey,
|
|
716
865
|
stimulusJsonSchema,
|
|
866
|
+
stimulusMediaKey,
|
|
717
867
|
validateActivity,
|
|
718
868
|
validateItemGroup,
|
|
719
869
|
validateXAPIStatement,
|