@intellectif/lk-core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/{activity-CPtJUBek.d.cts → activity-B0zbvu18.d.cts} +177 -2
- package/dist/{activity-CPtJUBek.d.ts → activity-B0zbvu18.d.ts} +177 -2
- package/dist/{chunk-YVZCFUGP.cjs → chunk-3FEAEO3D.cjs} +18 -13
- package/dist/chunk-3FEAEO3D.cjs.map +1 -0
- package/dist/{chunk-CVRALNUK.cjs → chunk-7ODQRNR4.cjs} +618 -45
- package/dist/chunk-7ODQRNR4.cjs.map +1 -0
- package/dist/{chunk-AHEUYOT3.js → chunk-FIS5KBCE.js} +670 -97
- package/dist/chunk-FIS5KBCE.js.map +1 -0
- package/dist/{chunk-ELTP5P6V.js → chunk-IXXMYJI7.js} +2 -2
- package/dist/{chunk-MOLL5KXO.cjs → chunk-VBM5H6P5.cjs} +8 -8
- package/dist/{chunk-MOLL5KXO.cjs.map → chunk-VBM5H6P5.cjs.map} +1 -1
- package/dist/{chunk-RL2PQLCY.cjs → chunk-VTTRXGDD.cjs} +4 -4
- package/dist/{chunk-RL2PQLCY.cjs.map → chunk-VTTRXGDD.cjs.map} +1 -1
- package/dist/{chunk-6DM2H6BD.js → chunk-XGCTAQSS.js} +7 -2
- package/dist/chunk-XGCTAQSS.js.map +1 -0
- package/dist/{chunk-QX7P3CHP.js → chunk-ZTY4UIUD.js} +2 -2
- package/dist/{index-gIN564mV.d.cts → index-DSfETm4b.d.ts} +321 -3
- package/dist/{index-BvrA8nIV.d.ts → index-u_rBLkuc.d.cts} +321 -3
- package/dist/index.cjs +53 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +25 -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 +24 -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 +1 -1
- package/dist/chunk-6DM2H6BD.js.map +0 -1
- package/dist/chunk-AHEUYOT3.js.map +0 -1
- package/dist/chunk-CVRALNUK.cjs.map +0 -1
- package/dist/chunk-YVZCFUGP.cjs.map +0 -1
- /package/dist/{chunk-ELTP5P6V.js.map → chunk-IXXMYJI7.js.map} +0 -0
- /package/dist/{chunk-QX7P3CHP.js.map → chunk-ZTY4UIUD.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @intellectif/lk-core
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a88b3f9: Multiple-choice options can carry a picture or a recording.
|
|
8
|
+
|
|
9
|
+
The A1/A2 picture-choice item ("which picture shows a cat?") and the minimal-pair listening item had no expression in the data contract: `media` sat above the question, one asset per activity, so four pictures as four options was not sayable.
|
|
10
|
+
|
|
11
|
+
`MultipleChoiceOption.media` adds it, additively — `text` stays required on every option, because it names the option in the accessible name and in the xAPI statement and is what the learner sees when a picture fails to load.
|
|
12
|
+
|
|
13
|
+
**Only `image` and `audio` are accepted.** `video` and `embed` are refused by the schema, and not out of caution: both render a control surface that swallows the click meant to select the option, so the learner could not choose it. An option also carries **no `playback` policy** — `maxPlays` binds per slot through a `MediaBudgetBinding`, and nothing has decided whether four recordings in one question share a budget or hold one each, so writing one is an error rather than a promise no renderer keeps.
|
|
14
|
+
|
|
15
|
+
A picture renders **inside** the option's label, so clicking it selects the option and its `alt` joins the accessible name. A recording renders **outside** the label, so pressing play does not commit the learner to that answer before they have heard the others. An option with no media renders exactly the markup it always did.
|
|
16
|
+
|
|
17
|
+
`alt` follows the rule activity media already uses — required and non-empty for an image, optional for audio. Note that on a picture-choice item the `alt` is part of the item: it can hand a screen-reader user the answer. The SDK requires it so an option is never silently inaccessible and leaves the wording to you; the authoring guide says so plainly.
|
|
18
|
+
|
|
19
|
+
Redaction keeps option media intact — it is what the learner picks — and removes only `isCorrect` and per-option `feedback`. Draft checks report an option's media under the codes media already uses (`media_url_required`, `media_alt_required`, `media_url_invalid`), plus one new code, `mc_option_media_kind`, for a refused `video` or `embed`.
|
|
20
|
+
|
|
3
21
|
## 0.9.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
@@ -359,6 +359,7 @@ interface ActivityDataMap {
|
|
|
359
359
|
'multiple-choice': MultipleChoiceData;
|
|
360
360
|
'fill-in-the-blanks': FillInTheBlanksData;
|
|
361
361
|
'written-response': WrittenResponseData;
|
|
362
|
+
'gap-select': GapSelectData;
|
|
362
363
|
}
|
|
363
364
|
/**
|
|
364
365
|
* The set of activity types supported by learning-kit. Derived from
|
|
@@ -447,15 +448,64 @@ interface ActivityFeedback {
|
|
|
447
448
|
incorrect?: string;
|
|
448
449
|
}
|
|
449
450
|
/** A single selectable option within a Multiple Choice activity. */
|
|
451
|
+
/**
|
|
452
|
+
* A picture or a recording carried by ONE multiple-choice option — the
|
|
453
|
+
* A1/A2 picture-choice item, and the minimal-pair listening item.
|
|
454
|
+
*
|
|
455
|
+
* Narrower than {@link ActivityMedia}, which sits above the question, and
|
|
456
|
+
* deliberately so. An option is a click target, and two of the activity-level
|
|
457
|
+
* kinds cannot be one:
|
|
458
|
+
*
|
|
459
|
+
* - **`embed` is refused.** A provider iframe swallows pointer events, so
|
|
460
|
+
* clicking the option would play the video instead of selecting the answer.
|
|
461
|
+
* The learner could not choose it at all.
|
|
462
|
+
* - **`video` is refused** for the same reason one step down: a native control
|
|
463
|
+
* bar inside the option's label eats the clicks meant for the radio.
|
|
464
|
+
* - **No `playback` policy.** `maxPlays` binds per slot through a
|
|
465
|
+
* `MediaBudgetBinding`, and four recordings in one question raise a question
|
|
466
|
+
* nothing has answered yet — whether the budget belongs to the option or to
|
|
467
|
+
* the item. Until something asks, an option plays through the browser's own
|
|
468
|
+
* bar and counts nothing.
|
|
469
|
+
*
|
|
470
|
+
* `alt` follows the same rule {@link ActivityMedia} uses, so there is only one
|
|
471
|
+
* rule to learn: required and non-empty for an image, optional for audio, where
|
|
472
|
+
* it becomes the player's accessible label.
|
|
473
|
+
*
|
|
474
|
+
* **On an image option, `alt` is part of the item.** It joins the option's
|
|
475
|
+
* `text` in the radio's accessible name, so on "which picture shows a cat?" an
|
|
476
|
+
* `alt` of "a cat" hands a screen-reader user the answer that a sighted learner
|
|
477
|
+
* has to work out. That is a property of picture-choice items, not something a
|
|
478
|
+
* schema can fix — the SDK requires `alt` so an option is never SILENTLY
|
|
479
|
+
* inaccessible, and leaves the wording, and the item's validity, to the author.
|
|
480
|
+
*/
|
|
481
|
+
interface MultipleChoiceOptionMedia {
|
|
482
|
+
/** `image` for a picture-choice option, `audio` for a listening option. */
|
|
483
|
+
type: 'image' | 'audio';
|
|
484
|
+
/** Address of the file. Same scheme allow-list as {@link ActivityMedia}. */
|
|
485
|
+
url: string;
|
|
486
|
+
/** Required and non-empty for `image`; an optional accessible label for `audio`. */
|
|
487
|
+
alt?: string;
|
|
488
|
+
/** WebVTT captions for an audio option, rendered as a `<track>`. */
|
|
489
|
+
captionsUrl?: string;
|
|
490
|
+
}
|
|
450
491
|
interface MultipleChoiceOption {
|
|
451
492
|
/** Unique identifier for this option within the activity. */
|
|
452
493
|
id: string;
|
|
453
|
-
/**
|
|
494
|
+
/**
|
|
495
|
+
* Display text shown to the learner.
|
|
496
|
+
*
|
|
497
|
+
* Required even when the option carries {@link media}: it names the option in
|
|
498
|
+
* the radio's accessible name and in the xAPI statement, and it is what the
|
|
499
|
+
* learner sees if a picture fails to load. For a pure picture-choice option,
|
|
500
|
+
* a neutral label ("Picture 1") keeps the naming out of the answer.
|
|
501
|
+
*/
|
|
454
502
|
text: string;
|
|
455
503
|
/** Whether this option is part of the correct answer. */
|
|
456
504
|
isCorrect: boolean;
|
|
457
505
|
/** Optional per-option feedback shown after submission. */
|
|
458
506
|
feedback?: string;
|
|
507
|
+
/** An optional picture or recording shown as part of this option. */
|
|
508
|
+
media?: MultipleChoiceOptionMedia;
|
|
459
509
|
}
|
|
460
510
|
/** Data contract for a Multiple Choice activity. */
|
|
461
511
|
interface MultipleChoiceData {
|
|
@@ -528,6 +578,116 @@ interface BlankConfig {
|
|
|
528
578
|
feedback?: string;
|
|
529
579
|
}
|
|
530
580
|
/** Data contract for a Fill-in-the-Blanks activity. */
|
|
581
|
+
/** One selectable option in a Gap Select activity. */
|
|
582
|
+
interface GapSelectChoice {
|
|
583
|
+
/** Unique identifier within the choice set the gap resolves to. */
|
|
584
|
+
id: string;
|
|
585
|
+
/** The text the learner reads in the selector. */
|
|
586
|
+
text: string;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* A named set of choices several gaps can draw from — a word bank.
|
|
590
|
+
*
|
|
591
|
+
* A bank is what makes distractors possible: three gaps sharing a bank of five
|
|
592
|
+
* choices means every selector offers all five, and two of them answer no gap
|
|
593
|
+
* at all. That is the difference between a reading-comprehension item and a
|
|
594
|
+
* sequence of three-way guesses, so it is modelled once here rather than
|
|
595
|
+
* retrofitted after per-gap lists ship.
|
|
596
|
+
*/
|
|
597
|
+
interface GapSelectBank {
|
|
598
|
+
/** Unique identifier referenced by {@link GapSelectGap.bankId}. */
|
|
599
|
+
id: string;
|
|
600
|
+
/** The choices every gap on this bank offers. */
|
|
601
|
+
choices: GapSelectChoice[];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* One gap in the passage, matching a `{{gap_id}}` placeholder.
|
|
605
|
+
*
|
|
606
|
+
* Its choices come from exactly one place: `choices` for a list of its own, or
|
|
607
|
+
* `bankId` for a shared word bank. Both, or neither, is an authoring error the
|
|
608
|
+
* schema rejects — with two sources a reader cannot tell which list the learner
|
|
609
|
+
* is offered, and the answer key means nothing without one.
|
|
610
|
+
*/
|
|
611
|
+
interface GapSelectGap {
|
|
612
|
+
/** Unique identifier matching the `{{gap_id}}` placeholder in the passage. */
|
|
613
|
+
id: string;
|
|
614
|
+
/** This gap's own choices. Mutually exclusive with {@link bankId}. */
|
|
615
|
+
choices?: GapSelectChoice[];
|
|
616
|
+
/** The {@link GapSelectBank} this gap draws from. Mutually exclusive with {@link choices}. */
|
|
617
|
+
bankId?: string;
|
|
618
|
+
/** The `id` of the one choice that is correct. Must exist in the resolved choice set. */
|
|
619
|
+
correctChoiceId: string;
|
|
620
|
+
/** Optional feedback shown inline next to this gap after submission. */
|
|
621
|
+
feedback?: string;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Data contract for a Gap Select activity: a passage whose gaps the learner
|
|
625
|
+
* fills by choosing from a list rather than typing.
|
|
626
|
+
*
|
|
627
|
+
* It is deliberately **not** a mode of {@link FillInTheBlanksData}, though the
|
|
628
|
+
* passage and its `{{id}}` placeholders are authored the same way. A learner
|
|
629
|
+
* picking from a selector cannot mistype, so the whole `TextMatchPolicy`
|
|
630
|
+
* surface — diacritic folding, typo tolerance, locale-aware case — is not just
|
|
631
|
+
* unused but misleading. Scoring is identity comparison of a choice id.
|
|
632
|
+
* Redaction inverts too: in Fill-in-the-Blanks the candidate answers ARE the
|
|
633
|
+
* key, while here the learner must be shown every choice and only
|
|
634
|
+
* `correctChoiceId` is withheld.
|
|
635
|
+
*/
|
|
636
|
+
interface GapSelectData {
|
|
637
|
+
schemaVersion: '1.0';
|
|
638
|
+
type: 'gap-select';
|
|
639
|
+
/** Unique identifier for this activity. */
|
|
640
|
+
id: string;
|
|
641
|
+
/** Human-readable title used in xAPI statements and error boundaries. */
|
|
642
|
+
title: string;
|
|
643
|
+
/** Passage text containing `{{gap_id}}` placeholders. */
|
|
644
|
+
passage: string;
|
|
645
|
+
/**
|
|
646
|
+
* Optional sanitised rich-HTML rendering of the passage. Carried and redacted
|
|
647
|
+
* as learner-visible content; not rendered by the SDK, for the same reason
|
|
648
|
+
* `FillInTheBlanksData.passageHtml` is not — slicing sanitised HTML at the
|
|
649
|
+
* placeholders to host the selectors is both lossy and unsafe.
|
|
650
|
+
*/
|
|
651
|
+
passageHtml?: string;
|
|
652
|
+
/** Every gap in the passage, one per distinct `{{id}}` placeholder. */
|
|
653
|
+
gaps: GapSelectGap[];
|
|
654
|
+
/** Shared word banks. Only needed by gaps that set `bankId`. */
|
|
655
|
+
banks?: GapSelectBank[];
|
|
656
|
+
/** Scoring algorithm applied when the learner submits. */
|
|
657
|
+
scoringStrategy: 'all-or-nothing' | 'partial';
|
|
658
|
+
/**
|
|
659
|
+
* How the choices are presented. Only `'dropdown'` exists today, and it is
|
|
660
|
+
* the default.
|
|
661
|
+
*
|
|
662
|
+
* The field is here rather than assumed because a drag-and-drop presentation
|
|
663
|
+
* is the obvious next request, and WCAG 2.5.7 requires that a drag interface
|
|
664
|
+
* always keep a non-drag path — so the choice has to be expressible in the
|
|
665
|
+
* content, not decided by a component prop. `'drag'` is NOT accepted yet:
|
|
666
|
+
* shipping a value nothing renders would freeze an API this repository has
|
|
667
|
+
* not validated. Widening the union later is additive.
|
|
668
|
+
*/
|
|
669
|
+
presentation?: 'dropdown';
|
|
670
|
+
/**
|
|
671
|
+
* Whether the SDK shuffles each selector's choices. Off by default.
|
|
672
|
+
*
|
|
673
|
+
* Like `MultipleChoiceData.shuffle`, this needs a seed to be reproducible at
|
|
674
|
+
* a remark — `<ActivitySequence>` refuses to shuffle without one outside
|
|
675
|
+
* `practice` mode.
|
|
676
|
+
*/
|
|
677
|
+
shuffleChoices?: boolean;
|
|
678
|
+
/** Optional media shown above the passage. */
|
|
679
|
+
media?: ActivityMedia;
|
|
680
|
+
/** Optional authored overall feedback shown after submission. */
|
|
681
|
+
feedback?: ActivityFeedback;
|
|
682
|
+
/** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
|
|
683
|
+
passThreshold?: number;
|
|
684
|
+
/** BCP 47 language tag for the activity content. */
|
|
685
|
+
locale?: string;
|
|
686
|
+
/** IRI references to learning objectives addressed by this activity. */
|
|
687
|
+
learningObjectives?: string[];
|
|
688
|
+
/** Subjective difficulty on a 1–5 scale. */
|
|
689
|
+
difficultyLevel?: 1 | 2 | 3 | 4 | 5;
|
|
690
|
+
}
|
|
531
691
|
interface FillInTheBlanksData {
|
|
532
692
|
schemaVersion: '1.0';
|
|
533
693
|
type: 'fill-in-the-blanks';
|
|
@@ -630,6 +790,7 @@ interface LearnerResponseMap {
|
|
|
630
790
|
'multiple-choice': MultipleChoiceLearnerResponse;
|
|
631
791
|
'fill-in-the-blanks': FillInTheBlanksLearnerResponse;
|
|
632
792
|
'written-response': WrittenResponseLearnerResponse;
|
|
793
|
+
'gap-select': GapSelectLearnerResponse;
|
|
633
794
|
}
|
|
634
795
|
/** Union of all learner response shapes. */
|
|
635
796
|
type LearnerResponse = LearnerResponseMap[keyof LearnerResponseMap];
|
|
@@ -645,6 +806,20 @@ interface FillInTheBlanksLearnerResponse {
|
|
|
645
806
|
/** Map of blank ID to the learner's typed answer. */
|
|
646
807
|
answers: Record<string, string>;
|
|
647
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Learner response for a Gap Select activity.
|
|
811
|
+
*
|
|
812
|
+
* A gap the learner has not answered is **absent from the map, or holds an
|
|
813
|
+
* empty string** — the two are equivalent, because a `<select>` whose
|
|
814
|
+
* placeholder is still showing submits `''`. Neither is a wrong answer: the
|
|
815
|
+
* scorer reports it as `incorrect-omission`, which is how a blank the learner
|
|
816
|
+
* never reached is told apart from one they got wrong.
|
|
817
|
+
*/
|
|
818
|
+
interface GapSelectLearnerResponse {
|
|
819
|
+
type: 'gap-select';
|
|
820
|
+
/** Map of gap ID to the ID of the choice the learner selected. */
|
|
821
|
+
selections: Record<string, string>;
|
|
822
|
+
}
|
|
648
823
|
/** Learner response for a Written Response activity. */
|
|
649
824
|
interface WrittenResponseLearnerResponse {
|
|
650
825
|
type: 'written-response';
|
|
@@ -845,4 +1020,4 @@ interface InteractionEvent {
|
|
|
845
1020
|
payload: Record<string, unknown>;
|
|
846
1021
|
}
|
|
847
1022
|
|
|
848
|
-
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type
|
|
1023
|
+
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIContextActivities as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIError as H, type InlineCorrection as I, type XAPIObject as J, type XAPIResult as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIScore as O, type XAPIStatement as P, type XAPIVerbObject as Q, levenshteinDistance as R, type ScoringDetail as S, type TextMatchPolicy as T, matchText as U, 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 MultipleChoiceOptionMedia as q, type ScoringOutcome as r, type ScoringResult as s, type TextMatchResult as t, type ValidationResult as u, type WrittenResponseLearnerResponse as v, type WrittenResponseRubric as w, type WrittenResponseRubricCriterion as x, type XAPIConfig as y, type XAPIContext as z };
|
|
@@ -359,6 +359,7 @@ interface ActivityDataMap {
|
|
|
359
359
|
'multiple-choice': MultipleChoiceData;
|
|
360
360
|
'fill-in-the-blanks': FillInTheBlanksData;
|
|
361
361
|
'written-response': WrittenResponseData;
|
|
362
|
+
'gap-select': GapSelectData;
|
|
362
363
|
}
|
|
363
364
|
/**
|
|
364
365
|
* The set of activity types supported by learning-kit. Derived from
|
|
@@ -447,15 +448,64 @@ interface ActivityFeedback {
|
|
|
447
448
|
incorrect?: string;
|
|
448
449
|
}
|
|
449
450
|
/** A single selectable option within a Multiple Choice activity. */
|
|
451
|
+
/**
|
|
452
|
+
* A picture or a recording carried by ONE multiple-choice option — the
|
|
453
|
+
* A1/A2 picture-choice item, and the minimal-pair listening item.
|
|
454
|
+
*
|
|
455
|
+
* Narrower than {@link ActivityMedia}, which sits above the question, and
|
|
456
|
+
* deliberately so. An option is a click target, and two of the activity-level
|
|
457
|
+
* kinds cannot be one:
|
|
458
|
+
*
|
|
459
|
+
* - **`embed` is refused.** A provider iframe swallows pointer events, so
|
|
460
|
+
* clicking the option would play the video instead of selecting the answer.
|
|
461
|
+
* The learner could not choose it at all.
|
|
462
|
+
* - **`video` is refused** for the same reason one step down: a native control
|
|
463
|
+
* bar inside the option's label eats the clicks meant for the radio.
|
|
464
|
+
* - **No `playback` policy.** `maxPlays` binds per slot through a
|
|
465
|
+
* `MediaBudgetBinding`, and four recordings in one question raise a question
|
|
466
|
+
* nothing has answered yet — whether the budget belongs to the option or to
|
|
467
|
+
* the item. Until something asks, an option plays through the browser's own
|
|
468
|
+
* bar and counts nothing.
|
|
469
|
+
*
|
|
470
|
+
* `alt` follows the same rule {@link ActivityMedia} uses, so there is only one
|
|
471
|
+
* rule to learn: required and non-empty for an image, optional for audio, where
|
|
472
|
+
* it becomes the player's accessible label.
|
|
473
|
+
*
|
|
474
|
+
* **On an image option, `alt` is part of the item.** It joins the option's
|
|
475
|
+
* `text` in the radio's accessible name, so on "which picture shows a cat?" an
|
|
476
|
+
* `alt` of "a cat" hands a screen-reader user the answer that a sighted learner
|
|
477
|
+
* has to work out. That is a property of picture-choice items, not something a
|
|
478
|
+
* schema can fix — the SDK requires `alt` so an option is never SILENTLY
|
|
479
|
+
* inaccessible, and leaves the wording, and the item's validity, to the author.
|
|
480
|
+
*/
|
|
481
|
+
interface MultipleChoiceOptionMedia {
|
|
482
|
+
/** `image` for a picture-choice option, `audio` for a listening option. */
|
|
483
|
+
type: 'image' | 'audio';
|
|
484
|
+
/** Address of the file. Same scheme allow-list as {@link ActivityMedia}. */
|
|
485
|
+
url: string;
|
|
486
|
+
/** Required and non-empty for `image`; an optional accessible label for `audio`. */
|
|
487
|
+
alt?: string;
|
|
488
|
+
/** WebVTT captions for an audio option, rendered as a `<track>`. */
|
|
489
|
+
captionsUrl?: string;
|
|
490
|
+
}
|
|
450
491
|
interface MultipleChoiceOption {
|
|
451
492
|
/** Unique identifier for this option within the activity. */
|
|
452
493
|
id: string;
|
|
453
|
-
/**
|
|
494
|
+
/**
|
|
495
|
+
* Display text shown to the learner.
|
|
496
|
+
*
|
|
497
|
+
* Required even when the option carries {@link media}: it names the option in
|
|
498
|
+
* the radio's accessible name and in the xAPI statement, and it is what the
|
|
499
|
+
* learner sees if a picture fails to load. For a pure picture-choice option,
|
|
500
|
+
* a neutral label ("Picture 1") keeps the naming out of the answer.
|
|
501
|
+
*/
|
|
454
502
|
text: string;
|
|
455
503
|
/** Whether this option is part of the correct answer. */
|
|
456
504
|
isCorrect: boolean;
|
|
457
505
|
/** Optional per-option feedback shown after submission. */
|
|
458
506
|
feedback?: string;
|
|
507
|
+
/** An optional picture or recording shown as part of this option. */
|
|
508
|
+
media?: MultipleChoiceOptionMedia;
|
|
459
509
|
}
|
|
460
510
|
/** Data contract for a Multiple Choice activity. */
|
|
461
511
|
interface MultipleChoiceData {
|
|
@@ -528,6 +578,116 @@ interface BlankConfig {
|
|
|
528
578
|
feedback?: string;
|
|
529
579
|
}
|
|
530
580
|
/** Data contract for a Fill-in-the-Blanks activity. */
|
|
581
|
+
/** One selectable option in a Gap Select activity. */
|
|
582
|
+
interface GapSelectChoice {
|
|
583
|
+
/** Unique identifier within the choice set the gap resolves to. */
|
|
584
|
+
id: string;
|
|
585
|
+
/** The text the learner reads in the selector. */
|
|
586
|
+
text: string;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* A named set of choices several gaps can draw from — a word bank.
|
|
590
|
+
*
|
|
591
|
+
* A bank is what makes distractors possible: three gaps sharing a bank of five
|
|
592
|
+
* choices means every selector offers all five, and two of them answer no gap
|
|
593
|
+
* at all. That is the difference between a reading-comprehension item and a
|
|
594
|
+
* sequence of three-way guesses, so it is modelled once here rather than
|
|
595
|
+
* retrofitted after per-gap lists ship.
|
|
596
|
+
*/
|
|
597
|
+
interface GapSelectBank {
|
|
598
|
+
/** Unique identifier referenced by {@link GapSelectGap.bankId}. */
|
|
599
|
+
id: string;
|
|
600
|
+
/** The choices every gap on this bank offers. */
|
|
601
|
+
choices: GapSelectChoice[];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* One gap in the passage, matching a `{{gap_id}}` placeholder.
|
|
605
|
+
*
|
|
606
|
+
* Its choices come from exactly one place: `choices` for a list of its own, or
|
|
607
|
+
* `bankId` for a shared word bank. Both, or neither, is an authoring error the
|
|
608
|
+
* schema rejects — with two sources a reader cannot tell which list the learner
|
|
609
|
+
* is offered, and the answer key means nothing without one.
|
|
610
|
+
*/
|
|
611
|
+
interface GapSelectGap {
|
|
612
|
+
/** Unique identifier matching the `{{gap_id}}` placeholder in the passage. */
|
|
613
|
+
id: string;
|
|
614
|
+
/** This gap's own choices. Mutually exclusive with {@link bankId}. */
|
|
615
|
+
choices?: GapSelectChoice[];
|
|
616
|
+
/** The {@link GapSelectBank} this gap draws from. Mutually exclusive with {@link choices}. */
|
|
617
|
+
bankId?: string;
|
|
618
|
+
/** The `id` of the one choice that is correct. Must exist in the resolved choice set. */
|
|
619
|
+
correctChoiceId: string;
|
|
620
|
+
/** Optional feedback shown inline next to this gap after submission. */
|
|
621
|
+
feedback?: string;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Data contract for a Gap Select activity: a passage whose gaps the learner
|
|
625
|
+
* fills by choosing from a list rather than typing.
|
|
626
|
+
*
|
|
627
|
+
* It is deliberately **not** a mode of {@link FillInTheBlanksData}, though the
|
|
628
|
+
* passage and its `{{id}}` placeholders are authored the same way. A learner
|
|
629
|
+
* picking from a selector cannot mistype, so the whole `TextMatchPolicy`
|
|
630
|
+
* surface — diacritic folding, typo tolerance, locale-aware case — is not just
|
|
631
|
+
* unused but misleading. Scoring is identity comparison of a choice id.
|
|
632
|
+
* Redaction inverts too: in Fill-in-the-Blanks the candidate answers ARE the
|
|
633
|
+
* key, while here the learner must be shown every choice and only
|
|
634
|
+
* `correctChoiceId` is withheld.
|
|
635
|
+
*/
|
|
636
|
+
interface GapSelectData {
|
|
637
|
+
schemaVersion: '1.0';
|
|
638
|
+
type: 'gap-select';
|
|
639
|
+
/** Unique identifier for this activity. */
|
|
640
|
+
id: string;
|
|
641
|
+
/** Human-readable title used in xAPI statements and error boundaries. */
|
|
642
|
+
title: string;
|
|
643
|
+
/** Passage text containing `{{gap_id}}` placeholders. */
|
|
644
|
+
passage: string;
|
|
645
|
+
/**
|
|
646
|
+
* Optional sanitised rich-HTML rendering of the passage. Carried and redacted
|
|
647
|
+
* as learner-visible content; not rendered by the SDK, for the same reason
|
|
648
|
+
* `FillInTheBlanksData.passageHtml` is not — slicing sanitised HTML at the
|
|
649
|
+
* placeholders to host the selectors is both lossy and unsafe.
|
|
650
|
+
*/
|
|
651
|
+
passageHtml?: string;
|
|
652
|
+
/** Every gap in the passage, one per distinct `{{id}}` placeholder. */
|
|
653
|
+
gaps: GapSelectGap[];
|
|
654
|
+
/** Shared word banks. Only needed by gaps that set `bankId`. */
|
|
655
|
+
banks?: GapSelectBank[];
|
|
656
|
+
/** Scoring algorithm applied when the learner submits. */
|
|
657
|
+
scoringStrategy: 'all-or-nothing' | 'partial';
|
|
658
|
+
/**
|
|
659
|
+
* How the choices are presented. Only `'dropdown'` exists today, and it is
|
|
660
|
+
* the default.
|
|
661
|
+
*
|
|
662
|
+
* The field is here rather than assumed because a drag-and-drop presentation
|
|
663
|
+
* is the obvious next request, and WCAG 2.5.7 requires that a drag interface
|
|
664
|
+
* always keep a non-drag path — so the choice has to be expressible in the
|
|
665
|
+
* content, not decided by a component prop. `'drag'` is NOT accepted yet:
|
|
666
|
+
* shipping a value nothing renders would freeze an API this repository has
|
|
667
|
+
* not validated. Widening the union later is additive.
|
|
668
|
+
*/
|
|
669
|
+
presentation?: 'dropdown';
|
|
670
|
+
/**
|
|
671
|
+
* Whether the SDK shuffles each selector's choices. Off by default.
|
|
672
|
+
*
|
|
673
|
+
* Like `MultipleChoiceData.shuffle`, this needs a seed to be reproducible at
|
|
674
|
+
* a remark — `<ActivitySequence>` refuses to shuffle without one outside
|
|
675
|
+
* `practice` mode.
|
|
676
|
+
*/
|
|
677
|
+
shuffleChoices?: boolean;
|
|
678
|
+
/** Optional media shown above the passage. */
|
|
679
|
+
media?: ActivityMedia;
|
|
680
|
+
/** Optional authored overall feedback shown after submission. */
|
|
681
|
+
feedback?: ActivityFeedback;
|
|
682
|
+
/** Minimum scaled score [0–1] required to pass. Defaults to {@link DEFAULT_PASS_THRESHOLD} (0.7) when absent. */
|
|
683
|
+
passThreshold?: number;
|
|
684
|
+
/** BCP 47 language tag for the activity content. */
|
|
685
|
+
locale?: string;
|
|
686
|
+
/** IRI references to learning objectives addressed by this activity. */
|
|
687
|
+
learningObjectives?: string[];
|
|
688
|
+
/** Subjective difficulty on a 1–5 scale. */
|
|
689
|
+
difficultyLevel?: 1 | 2 | 3 | 4 | 5;
|
|
690
|
+
}
|
|
531
691
|
interface FillInTheBlanksData {
|
|
532
692
|
schemaVersion: '1.0';
|
|
533
693
|
type: 'fill-in-the-blanks';
|
|
@@ -630,6 +790,7 @@ interface LearnerResponseMap {
|
|
|
630
790
|
'multiple-choice': MultipleChoiceLearnerResponse;
|
|
631
791
|
'fill-in-the-blanks': FillInTheBlanksLearnerResponse;
|
|
632
792
|
'written-response': WrittenResponseLearnerResponse;
|
|
793
|
+
'gap-select': GapSelectLearnerResponse;
|
|
633
794
|
}
|
|
634
795
|
/** Union of all learner response shapes. */
|
|
635
796
|
type LearnerResponse = LearnerResponseMap[keyof LearnerResponseMap];
|
|
@@ -645,6 +806,20 @@ interface FillInTheBlanksLearnerResponse {
|
|
|
645
806
|
/** Map of blank ID to the learner's typed answer. */
|
|
646
807
|
answers: Record<string, string>;
|
|
647
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Learner response for a Gap Select activity.
|
|
811
|
+
*
|
|
812
|
+
* A gap the learner has not answered is **absent from the map, or holds an
|
|
813
|
+
* empty string** — the two are equivalent, because a `<select>` whose
|
|
814
|
+
* placeholder is still showing submits `''`. Neither is a wrong answer: the
|
|
815
|
+
* scorer reports it as `incorrect-omission`, which is how a blank the learner
|
|
816
|
+
* never reached is told apart from one they got wrong.
|
|
817
|
+
*/
|
|
818
|
+
interface GapSelectLearnerResponse {
|
|
819
|
+
type: 'gap-select';
|
|
820
|
+
/** Map of gap ID to the ID of the choice the learner selected. */
|
|
821
|
+
selections: Record<string, string>;
|
|
822
|
+
}
|
|
648
823
|
/** Learner response for a Written Response activity. */
|
|
649
824
|
interface WrittenResponseLearnerResponse {
|
|
650
825
|
type: 'written-response';
|
|
@@ -845,4 +1020,4 @@ interface InteractionEvent {
|
|
|
845
1020
|
payload: Record<string, unknown>;
|
|
846
1021
|
}
|
|
847
1022
|
|
|
848
|
-
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type
|
|
1023
|
+
export { type ActivityData as A, type BlankConfig as B, type CriterionScore as C, type DeferredScoringPartial as D, type XAPIContextActivities as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIError as H, type InlineCorrection as I, type XAPIObject as J, type XAPIResult as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIScore as O, type XAPIStatement as P, type XAPIVerbObject as Q, levenshteinDistance as R, type ScoringDetail as S, type TextMatchPolicy as T, matchText as U, 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 MultipleChoiceOptionMedia as q, type ScoringOutcome as r, type ScoringResult as s, type TextMatchResult as t, type ValidationResult as u, type WrittenResponseLearnerResponse as v, type WrittenResponseRubric as w, type WrittenResponseRubricCriterion as x, type XAPIConfig as y, type XAPIContext as z };
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
var _chunk7ODQRNR4cjs = require('./chunk-7ODQRNR4.cjs');
|
|
10
11
|
|
|
11
12
|
// src/schemas/item-group.ts
|
|
12
13
|
var _v4 = require('zod/v4');
|
|
@@ -34,7 +35,7 @@ var StimulusSchema = _v4.z.looseObject({
|
|
|
34
35
|
title: _v4.z.string().optional(),
|
|
35
36
|
body: _v4.z.string().optional(),
|
|
36
37
|
bodyHtml: _v4.z.string().optional(),
|
|
37
|
-
media:
|
|
38
|
+
media: _chunk7ODQRNR4cjs.MediaSchema.optional(),
|
|
38
39
|
transcript: _v4.z.string().optional(),
|
|
39
40
|
locale: _v4.z.string().optional(),
|
|
40
41
|
attribution: _v4.z.string().optional()
|
|
@@ -84,7 +85,7 @@ var RedactedStimulusSchema = _v4.z.strictObject({
|
|
|
84
85
|
title: _v4.z.string().optional(),
|
|
85
86
|
body: _v4.z.string().optional(),
|
|
86
87
|
bodyHtml: _v4.z.string().optional(),
|
|
87
|
-
media:
|
|
88
|
+
media: _chunk7ODQRNR4cjs.RedactedMediaSchema.optional(),
|
|
88
89
|
locale: _v4.z.string().optional(),
|
|
89
90
|
attribution: _v4.z.string().optional()
|
|
90
91
|
});
|
|
@@ -114,7 +115,7 @@ function validateItemGroup(data) {
|
|
|
114
115
|
const errors = [];
|
|
115
116
|
const items = [];
|
|
116
117
|
container.data.items.forEach((item, index) => {
|
|
117
|
-
const descriptor =
|
|
118
|
+
const descriptor = _chunk7ODQRNR4cjs.getActivityTypeDescriptor.call(void 0, item.type);
|
|
118
119
|
if (descriptor === void 0) {
|
|
119
120
|
errors.push({
|
|
120
121
|
path: ["items", String(index), "type"],
|
|
@@ -140,28 +141,31 @@ function validateItemGroup(data) {
|
|
|
140
141
|
|
|
141
142
|
var stimulusJsonSchema = _v4.z.toJSONSchema(StimulusSchema, { target: "draft-7" });
|
|
142
143
|
var itemGroupJsonSchema = _v4.z.toJSONSchema(ItemGroupSchema, { target: "draft-7" });
|
|
143
|
-
var
|
|
144
|
+
var gapSelectJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.GapSelectDataSchema, {
|
|
145
|
+
target: "draft-7"
|
|
146
|
+
});
|
|
147
|
+
var multipleChoiceJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.MultipleChoiceDataSchema, {
|
|
144
148
|
target: "draft-7"
|
|
145
149
|
});
|
|
146
|
-
var fillInTheBlanksJsonSchema = _v4.z.toJSONSchema(
|
|
150
|
+
var fillInTheBlanksJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.FillInTheBlanksDataSchema, {
|
|
147
151
|
target: "draft-7"
|
|
148
152
|
});
|
|
149
|
-
var writtenResponseJsonSchema = _v4.z.toJSONSchema(
|
|
153
|
+
var writtenResponseJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.WrittenResponseDataSchema, {
|
|
150
154
|
target: "draft-7"
|
|
151
155
|
});
|
|
152
156
|
function jsonSchemaFor(type) {
|
|
153
|
-
const descriptor =
|
|
157
|
+
const descriptor = _chunk7ODQRNR4cjs.getActivityTypeDescriptor.call(void 0, type);
|
|
154
158
|
if (descriptor === void 0) {
|
|
155
|
-
throw new (0,
|
|
159
|
+
throw new (0, _chunk7ODQRNR4cjs.UnknownActivityTypeError)(type);
|
|
156
160
|
}
|
|
157
161
|
return _v4.z.toJSONSchema(descriptor.schema, { target: "draft-7" });
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
// src/schemas/index.ts
|
|
161
165
|
function validateActivity(type, data) {
|
|
162
|
-
const descriptor =
|
|
166
|
+
const descriptor = _chunk7ODQRNR4cjs.getActivityTypeDescriptor.call(void 0, type);
|
|
163
167
|
if (descriptor === void 0) {
|
|
164
|
-
throw new (0,
|
|
168
|
+
throw new (0, _chunk7ODQRNR4cjs.UnknownActivityTypeError)(String(type));
|
|
165
169
|
}
|
|
166
170
|
const result = descriptor.schema.safeParse(data);
|
|
167
171
|
if (result.success) {
|
|
@@ -190,5 +194,6 @@ function validateActivity(type, data) {
|
|
|
190
194
|
|
|
191
195
|
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
|
|
198
|
+
exports.StimulusSchema = StimulusSchema; exports.ItemGroupSchema = ItemGroupSchema; exports.RedactedStimulusSchema = RedactedStimulusSchema; exports.RedactedItemGroupSchema = RedactedItemGroupSchema; exports.validateItemGroup = validateItemGroup; exports.stimulusJsonSchema = stimulusJsonSchema; exports.itemGroupJsonSchema = itemGroupJsonSchema; exports.gapSelectJsonSchema = gapSelectJsonSchema; exports.multipleChoiceJsonSchema = multipleChoiceJsonSchema; exports.fillInTheBlanksJsonSchema = fillInTheBlanksJsonSchema; exports.writtenResponseJsonSchema = writtenResponseJsonSchema; exports.jsonSchemaFor = jsonSchemaFor; exports.validateActivity = validateActivity;
|
|
199
|
+
//# sourceMappingURL=chunk-3FEAEO3D.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-3FEAEO3D.cjs","../src/schemas/item-group.ts","../src/schemas/json-schema.ts","../src/schemas/index.ts"],"names":["z"],"mappings":"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","file":"/home/runner/work/learning-kit/learning-kit/packages/lk-core/dist/chunk-3FEAEO3D.cjs","sourcesContent":[null,"import { z } from 'zod/v4';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport type { ActivityData, ValidationError, ValidationResult } from '../types/activity.js';\nimport type { ItemGroup, StimulusKind } from '../types/item-group.js';\nimport { MediaSchema, RedactedMediaSchema } from './media.js';\n\n/**\n * An authored slot key.\n *\n * A `.` is rejected here, not at render time: `flattenSequence` uses the first\n * `.` to separate a group from its item, so a key containing one would produce\n * an ambiguous slot id. `keyOf` already threw on it — this moves the rejection\n * to `validateItemGroup`, where an author finds it, instead of leaving it to\n * surface when a learner opens the paper. Content that would newly fail\n * validation already threw at flatten time, so it can never have been sat.\n */\nconst SlotKeySchema = z\n .string()\n .min(1)\n .refine((key) => !key.includes('.'), {\n error:\n 'slotKey must not contain \".\" — that character separates a group from its item in a slot id, so a key containing one would make the slot ambiguous.',\n });\n\nfunction hasBody(stimulus: { body?: string | undefined }): boolean {\n return typeof stimulus.body === 'string' && stimulus.body.trim().length > 0;\n}\n\n/** Which media types can carry a stimulus of each kind. `text`/`mixed` accept any. */\nfunction mediaFits(kind: StimulusKind, mediaType: string | undefined): boolean {\n switch (kind) {\n case 'audio':\n return mediaType === 'audio';\n case 'video':\n return mediaType === 'video' || mediaType === 'embed';\n case 'image':\n return mediaType === 'image';\n default:\n return true;\n }\n}\n\n/**\n * Zod schema for a {@link Stimulus}. Loose: unknown keys preserved.\n *\n * The semantic guards exist because a stimulus that does not carry what its\n * `kind` promises is an authoring error that must not reach an exam: an\n * \"audio\" stimulus with no recording renders as a blank panel above six\n * listening questions. Unrepresentable in JSON Schema; dropped from\n * `stimulusJsonSchema` by design.\n */\nexport const StimulusSchema = z\n .looseObject({\n id: z.string().min(1),\n kind: z.enum(['text', 'audio', 'video', 'image', 'mixed']),\n title: z.string().optional(),\n body: z.string().optional(),\n bodyHtml: z.string().optional(),\n media: MediaSchema.optional(),\n transcript: z.string().optional(),\n locale: z.string().optional(),\n attribution: z.string().optional(),\n })\n .refine((stimulus) => stimulus.bodyHtml === undefined || hasBody(stimulus), {\n error:\n 'bodyHtml requires a plain-text body: it is the accessible fallback rendered when no sanitiser is supplied.',\n path: ['body'],\n })\n .refine(\n (stimulus) => (stimulus.kind !== 'text' && stimulus.kind !== 'mixed') || hasBody(stimulus),\n {\n error: 'A text or mixed stimulus needs a non-empty body.',\n path: ['body'],\n },\n )\n .refine((stimulus) => stimulus.kind === 'text' || stimulus.media !== undefined, {\n error: 'An audio, video, image or mixed stimulus needs media.',\n path: ['media'],\n })\n .refine(\n (stimulus) => stimulus.media === undefined || mediaFits(stimulus.kind, stimulus.media.type),\n {\n error:\n 'media.type does not fit the stimulus kind: audio needs audio; video needs video or embed; image needs image.',\n path: ['media', 'type'],\n },\n );\n\n/**\n * The structural minimum of an item as seen by the CONTAINER schema. Each\n * item's own contract is checked against its registered schema by\n * {@link validateItemGroup} — a zod schema cannot dispatch on a registry\n * that consumers extend at runtime.\n */\nconst ItemShapeSchema = z.looseObject({\n type: z.string().min(1),\n id: z.string().min(1),\n slotKey: SlotKeySchema.optional(),\n});\n\n/**\n * Zod schema for an {@link ItemGroup} CONTAINER. Loose: unknown keys\n * preserved. Validates the group's own fields and the stimulus in full, and\n * each item only structurally (`type` and `id`); use {@link validateItemGroup}\n * to validate the items against their registered schemas as well.\n */\nexport const ItemGroupSchema = z\n .looseObject({\n schemaVersion: z.literal('1.0'),\n type: z.literal('item-group'),\n id: z.string().min(1),\n title: z.string().optional(),\n slotKey: SlotKeySchema.optional(),\n stimulus: StimulusSchema,\n items: z.array(ItemShapeSchema).min(1),\n shuffle: z.enum(['none', 'within-group']).optional(),\n })\n .refine((group) => group.items.every((item) => item.type !== 'item-group'), {\n error: 'Item groups do not nest: every item must be an activity.',\n path: ['items'],\n })\n .refine((group) => new Set(group.items.map((item) => item.id)).size === group.items.length, {\n error: 'Item ids must be unique within a group.',\n path: ['items'],\n });\n\n/** The learner-safe shape of a stimulus, derived from the strict schema below. */\nexport type RedactedStimulus = z.infer<typeof RedactedStimulusSchema>;\n\n/** Strict learner-safe stimulus: everything but the author-only `transcript`. */\nexport const RedactedStimulusSchema = z.strictObject({\n id: z.string().min(1),\n kind: z.enum(['text', 'audio', 'video', 'image', 'mixed']),\n title: z.string().optional(),\n body: z.string().optional(),\n bodyHtml: z.string().optional(),\n media: RedactedMediaSchema.optional(),\n locale: z.string().optional(),\n attribution: z.string().optional(),\n});\n\n/**\n * Strict learner-safe item group. Items are left opaque here — each is proven\n * learner-safe by `assertRedacted` against its OWN type's redacted schema,\n * which is the only place that knowledge lives.\n */\nexport const RedactedItemGroupSchema = z.strictObject({\n redacted: z.literal(true),\n schemaVersion: z.literal('1.0'),\n type: z.literal('item-group'),\n id: z.string().min(1),\n title: z.string().optional(),\n slotKey: SlotKeySchema.optional(),\n stimulus: RedactedStimulusSchema,\n items: z.array(z.unknown()).min(1),\n shuffle: z.enum(['none', 'within-group']).optional(),\n});\n\nfunction toValidationErrors(\n issues: readonly { path: PropertyKey[]; message: string; code: string }[],\n prefix: string[],\n): ValidationError[] {\n return issues.map((issue) => ({\n path: [...prefix, ...issue.path.map(String)],\n message: issue.message,\n code: issue.code,\n }));\n}\n\n/**\n * Validates an item group in full: the container and stimulus against\n * {@link ItemGroupSchema}, then every item against the schema registered for\n * its `type`. Errors from items are reported at `items.<index>.…`.\n *\n * Unlike `validateActivity`, an item whose type is not registered is REPORTED\n * (code `unknown_activity_type`) rather than thrown: a group is validated as\n * a whole, and an author fixing a six-item group wants every problem listed,\n * not the first one that happened to throw.\n */\nexport function validateItemGroup(data: unknown): ValidationResult<ItemGroup> {\n const container = ItemGroupSchema.safeParse(data);\n if (!container.success) {\n return { success: false, errors: toValidationErrors(container.error.issues, []) };\n }\n\n const errors: ValidationError[] = [];\n const items: ActivityData[] = [];\n container.data.items.forEach((item, index) => {\n const descriptor = getActivityTypeDescriptor(item.type);\n if (descriptor === undefined) {\n errors.push({\n path: ['items', String(index), 'type'],\n message: `Activity type \"${item.type}\" is not registered`,\n code: 'unknown_activity_type',\n });\n return;\n }\n const parsed = descriptor.schema.safeParse(item);\n if (!parsed.success) {\n errors.push(...toValidationErrors(parsed.error.issues, ['items', String(index)]));\n return;\n }\n items.push(parsed.data as ActivityData);\n });\n\n if (errors.length > 0) {\n return { success: false, errors };\n }\n // zod4 optional outputs are `T | undefined`; the wire type uses exact\n // optionals. Structurally identical at runtime — the cast is type-level.\n return { success: true, data: { ...container.data, items } as unknown as ItemGroup };\n}\n","import { z } from 'zod/v4';\nimport { UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport { FillInTheBlanksDataSchema } from './fill-in-the-blanks.js';\nimport { GapSelectDataSchema } from './gap-select.js';\nimport { ItemGroupSchema, StimulusSchema } from './item-group.js';\nimport { MultipleChoiceDataSchema } from './multiple-choice.js';\nimport { WrittenResponseDataSchema } from './written-response.js';\n\n/**\n * JSON Schema (Draft 7) for a `Stimulus`. Structural contract only — the\n * kind/media/body consistency guards are Zod-only.\n */\nexport const stimulusJsonSchema = z.toJSONSchema(StimulusSchema, { target: 'draft-7' });\n\n/**\n * JSON Schema (Draft 7) for an `ItemGroup` CONTAINER. Items appear as objects\n * with `type` and `id` only; each item's own contract is `jsonSchemaFor(type)`.\n * For an AI generation pipeline, ask for the group and each item separately\n * rather than a single nested schema — that keeps the per-type schema the\n * registry's, not a copy.\n */\nexport const itemGroupJsonSchema = z.toJSONSchema(ItemGroupSchema, { target: 'draft-7' });\n\n/**\n * JSON Schema (Draft 7) representation of the Multiple Choice activity data\n * contract, generated natively by Zod 4. Draft 7 is mandated by Requirement\n * 2.2 for the widest AI-prompt / OpenAPI tooling compatibility. The semantic\n * `.refine()` guards are not representable in JSON Schema and are\n * intentionally omitted — the export captures the *structural* contract only.\n * Since v0.3 the source schemas are loose, so these no longer emit\n * `additionalProperties: false` — the JSON Schema and `validateActivity` now\n * agree on unknown-key handling.\n */\n/**\n * JSON Schema (Draft 7) representation of the Gap Select activity data\n * contract. Structural contract only: the six semantic guards — the\n * passage/gap bijection, id uniqueness, the one-choice-source rule, and an\n * answer key that names a choice the gap offers — are Zod-only, as they are\n * for the other types.\n */\nexport const gapSelectJsonSchema = z.toJSONSchema(GapSelectDataSchema, {\n target: 'draft-7',\n});\n\nexport const multipleChoiceJsonSchema = z.toJSONSchema(MultipleChoiceDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * JSON Schema (Draft 7) representation of the Fill-in-the-Blanks activity data\n * contract, generated natively by Zod 4. Structural contract only (semantic\n * `.refine()` guards are Zod-only and not representable in JSON Schema).\n */\nexport const fillInTheBlanksJsonSchema = z.toJSONSchema(FillInTheBlanksDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * JSON Schema (Draft 7) representation of the Written Response activity data\n * contract. Structural contract only.\n */\nexport const writtenResponseJsonSchema = z.toJSONSchema(WrittenResponseDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * Derives the JSON Schema (Draft 7) for any REGISTERED activity type — the\n * live, registry-backed replacement for the static per-type exports above,\n * and the building block for AI generation pipelines (R6.1): pass the result\n * as a structured-output schema so a model can only emit valid items.\n *\n * @throws UnknownActivityTypeError when `type` has no registered descriptor.\n */\nexport function jsonSchemaFor(type: string): Record<string, unknown> {\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(type);\n }\n return z.toJSONSchema(descriptor.schema as never, { target: 'draft-7' }) as Record<\n string,\n unknown\n >;\n}\n","import { UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport type { ActivityDataMap, ActivityType, ValidationResult } from '../types/activity.js';\n\nexport { FeedbackSchema } from './feedback.js';\nexport {\n BlankConfigSchema,\n FillInTheBlanksDataSchema,\n TextMatchPolicySchema,\n} from './fill-in-the-blanks.js';\nexport {\n GapSelectBankSchema,\n GapSelectChoiceSchema,\n GapSelectDataSchema,\n GapSelectGapSchema,\n} from './gap-select.js';\nexport type { RedactedStimulus } from './item-group.js';\nexport {\n ItemGroupSchema,\n RedactedItemGroupSchema,\n RedactedStimulusSchema,\n StimulusSchema,\n validateItemGroup,\n} from './item-group.js';\nexport {\n fillInTheBlanksJsonSchema,\n gapSelectJsonSchema,\n itemGroupJsonSchema,\n jsonSchemaFor,\n multipleChoiceJsonSchema,\n stimulusJsonSchema,\n writtenResponseJsonSchema,\n} from './json-schema.js';\nexport {\n MediaPlaybackSchema,\n MediaSchema,\n MediaUrlSchema,\n NativeControlHintSchema,\n RedactedMediaSchema,\n} from './media.js';\nexport {\n MultipleChoiceDataSchema,\n MultipleChoiceOptionMediaSchema,\n MultipleChoiceOptionSchema,\n} from './multiple-choice.js';\nexport type {\n RedactedActivity,\n RedactedBlankConfig,\n RedactedFillInTheBlanksData,\n RedactedGapSelectBank,\n RedactedGapSelectChoice,\n RedactedGapSelectData,\n RedactedGapSelectGap,\n RedactedMultipleChoiceData,\n RedactedMultipleChoiceOption,\n RedactedMultipleChoiceOptionMedia,\n RedactedWrittenResponseData,\n} from './redacted.js';\nexport {\n RedactedBlankConfigSchema,\n RedactedFillInTheBlanksDataSchema,\n RedactedGapSelectBankSchema,\n RedactedGapSelectChoiceSchema,\n RedactedGapSelectDataSchema,\n RedactedGapSelectGapSchema,\n RedactedMultipleChoiceDataSchema,\n RedactedMultipleChoiceOptionMediaSchema,\n RedactedMultipleChoiceOptionSchema,\n RedactedWrittenResponseDataSchema,\n} from './redacted.js';\nexport {\n WrittenResponseDataSchema,\n WrittenResponseRubricCriterionSchema,\n WrittenResponseRubricSchema,\n} from './written-response.js';\n\n/**\n * Validates raw activity data against the schema registered for the given\n * activity type (built-in or consumer-registered via `registerActivityType`).\n *\n * Unknown keys are PRESERVED, not stripped: every built-in schema is loose,\n * so consumer sidecar fields and forward-version fields survive validation\n * verbatim in the returned `data`.\n *\n * @returns `{ success: true, data }` with the typed, validated data, or\n * `{ success: false, errors }` with one entry per failed constraint.\n * @throws UnknownActivityTypeError when `type` has no registered descriptor.\n */\nexport function validateActivity<T extends ActivityType>(\n type: T,\n data: unknown,\n): ValidationResult<ActivityDataMap[T]> {\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(type));\n }\n\n const result = descriptor.schema.safeParse(data);\n\n if (result.success) {\n return { success: true, data: result.data as ActivityDataMap[T] };\n }\n\n return {\n success: false,\n errors: result.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n };\n}\n"]}
|