@intellectif/lk-core 0.8.2 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +5 -4
  3. package/dist/{activity-CPtJUBek.d.cts → activity-B0zbvu18.d.cts} +177 -2
  4. package/dist/{activity-CPtJUBek.d.ts → activity-B0zbvu18.d.ts} +177 -2
  5. package/dist/{chunk-FVLKIL6W.cjs → chunk-3FEAEO3D.cjs} +18 -13
  6. package/dist/chunk-3FEAEO3D.cjs.map +1 -0
  7. package/dist/chunk-7ODQRNR4.cjs +2009 -0
  8. package/dist/chunk-7ODQRNR4.cjs.map +1 -0
  9. package/dist/chunk-FIS5KBCE.js +2009 -0
  10. package/dist/chunk-FIS5KBCE.js.map +1 -0
  11. package/dist/{chunk-NHGXOOZ2.js → chunk-IXXMYJI7.js} +2 -2
  12. package/dist/{chunk-YJZY5TPY.cjs → chunk-VBM5H6P5.cjs} +8 -8
  13. package/dist/{chunk-YJZY5TPY.cjs.map → chunk-VBM5H6P5.cjs.map} +1 -1
  14. package/dist/{chunk-7ACNBDSF.cjs → chunk-VTTRXGDD.cjs} +4 -4
  15. package/dist/{chunk-7ACNBDSF.cjs.map → chunk-VTTRXGDD.cjs.map} +1 -1
  16. package/dist/{chunk-2M76F32Y.js → chunk-XGCTAQSS.js} +7 -2
  17. package/dist/chunk-XGCTAQSS.js.map +1 -0
  18. package/dist/{chunk-LNA33IK7.js → chunk-ZTY4UIUD.js} +2 -2
  19. package/dist/{index-BvrA8nIV.d.ts → index-DSfETm4b.d.ts} +321 -3
  20. package/dist/{index-gIN564mV.d.cts → index-u_rBLkuc.d.cts} +321 -3
  21. package/dist/index.cjs +156 -42
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +159 -8
  24. package/dist/index.d.ts +159 -8
  25. package/dist/index.js +137 -23
  26. package/dist/index.js.map +1 -1
  27. package/dist/schemas.cjs +25 -3
  28. package/dist/schemas.cjs.map +1 -1
  29. package/dist/schemas.d.cts +2 -2
  30. package/dist/schemas.d.ts +2 -2
  31. package/dist/schemas.js +24 -2
  32. package/dist/scoring.cjs +3 -3
  33. package/dist/scoring.d.cts +2 -2
  34. package/dist/scoring.d.ts +2 -2
  35. package/dist/scoring.js +2 -2
  36. package/dist/xapi.cjs +3 -3
  37. package/dist/xapi.d.cts +1 -1
  38. package/dist/xapi.d.ts +1 -1
  39. package/dist/xapi.js +2 -2
  40. package/package.json +1 -1
  41. package/dist/chunk-2M76F32Y.js.map +0 -1
  42. package/dist/chunk-FKH4YT5X.cjs +0 -748
  43. package/dist/chunk-FKH4YT5X.cjs.map +0 -1
  44. package/dist/chunk-FVLKIL6W.cjs.map +0 -1
  45. package/dist/chunk-HCOWSGEZ.js +0 -748
  46. package/dist/chunk-HCOWSGEZ.js.map +0 -1
  47. /package/dist/{chunk-NHGXOOZ2.js.map → chunk-IXXMYJI7.js.map} +0 -0
  48. /package/dist/{chunk-LNA33IK7.js.map → chunk-ZTY4UIUD.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
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
+
21
+ ## 0.9.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 8ca07c8: Authoring: tell an unfinished question from a wrong one.
26
+
27
+ `validateActivity` answers "may this be stored?", and to that question a question an author added a second ago and a broken one get the same answer. An editor that used it as its only check reported every new question as an error — enough to disable a whole form's Save button and blank its preview.
28
+
29
+ **lk-core**
30
+
31
+ - `validateDraft(type, draft)` returns `{ status: 'complete', data, issues: [] }` or `{ status: 'incomplete' | 'invalid', issues }`. Each issue is a `ValidationError` plus a `severity`, at the paths `validateActivity` reports. Its `code` is documented per type in the authoring guide for every problem the checks recognise; anything else is the schema's own diagnostic, at `invalid`. There is deliberately no `success` boolean, which would have to call an incomplete draft either a success or a failure.
32
+ - It is stricter than `validateActivity` and never looser. It reports a title or option text that is only whitespace, a blank written-response `prompt` even beside `promptHtml`, a rubric criterion name that is only whitespace, a rubric whose weights are all 0 or add up to more than a number can hold, a fill-in-the-blanks match `locale` that is not a language tag (scoring would throw on it), and `redacted: true`. A `null` in a required field reads as not set; any other `null` the schema refuses is `null_not_allowed`, for every registered type. It does not report `minWords: 0`, which means no lower limit, or a rubric weight above 1.
33
+ - `createDraft(type, { newId })` returns an empty draft that `validateDraft` reports as `incomplete`. It invents no ids, and a new multiple-choice draft marks no option correct, so an untouched correctness control can never become an answer key.
34
+ - `ActivityTypeDescriptor.authoring` — `createDraft` and `checkDraft` — gives a registered type the same support. The three built-in descriptors carry one.
35
+
36
+ **lk-react**
37
+
38
+ - `<ActivityPreview>`, also at `@intellectif/lk-react/components/ActivityPreview`, renders a draft in any `renderMode` with a simulated `response` — marked with `evaluate()` in `review` — and renders a notice, or your `fallback` with the issues, while the draft is not complete. Nothing it renders is recorded, and a recording's play limit is enforced in memory in `practice` and `exam`. It checks the draft by content on every render, whatever order its keys are in, so an editor that rebuilds or reloads its payload does not restart the question being tried.
39
+ - `LkStrings` gains `previewIncomplete` and `previewInvalid`. A dictionary declared as a complete `LkStrings` must add both; partial overrides are unaffected.
40
+
41
+ Nothing that already exists changes behaviour in either package. lk-react's major version comes from its lk-core peer range.
42
+
3
43
  ## 0.8.2
4
44
 
5
45
  ### Patch Changes
package/README.md CHANGED
@@ -16,7 +16,8 @@ npm install @intellectif/lk-core
16
16
 
17
17
  - **Activity schemas** for `multiple-choice`, `fill-in-the-blanks` and `written-response`, with semantic refinements (e.g. *at least one correct option*, *single-select ⇒ exactly one correct*, *passage ↔ blank-id bijection*, *image/embed require `alt`*). Loose at every level, so your sidecar fields survive validation.
18
18
  - **`validateActivity(type, data)`** — the authoritative runtime validator. Returns a typed `{ success, data }` or a structured `{ success: false, errors[] }`, and **throws `UnknownActivityTypeError` if `type` is not registered** — so guard it when validating a content bank that may carry types this build does not know. `validateItemGroup(data)` is the equivalent for a `Stimulus` + `ItemGroup` container, which `validateActivity` cannot take.
19
- - **An open type system** — `defineActivityType` / `registerActivityType` make an activity type a *value*, not a hard-coded union member: register one and `validateActivity`, `score`, `evaluate`, `redact` and `jsonSchemaFor` all work for it, with no SDK release.
19
+ - **`validateDraft(type, draft)`**for editors. Reports `complete`, `incomplete` (something not written yet) or `invalid` (something wrong), each issue with a `severity` and a `code` documented for every problem the checks recognise and deliberately no `success` boolean to misread. Stricter than `validateActivity`, never looser. **`createDraft(type, { newId })`** returns an empty draft to start from; a multiple-choice draft marks no option correct.
20
+ - **An open type system** — `defineActivityType` / `registerActivityType` make an activity type a *value*, not a hard-coded union member: register one and `validateActivity`, `validateDraft`, `score`, `evaluate`, `redact` and `jsonSchemaFor` all work for it, with no SDK release. An `authoring` block on the descriptor gives it draft support.
20
21
  - **JSON Schema (Draft-7) export** — `jsonSchemaFor(type)` for any registered type (plus the static per-type exports), suitable for form generators or AI prompting.
21
22
  - **Scoring engine** — `score(activityType, data, response)` returning `{ score ∈ [0,1], maxScore: 1, passed, feedback, details }`. Pure, deterministic, 100 % test coverage enforced. It **throws** rather than inventing a number: `DeferredScoringError` for a deferred-graded type (`written-response`), and `RedactedScoringError` when handed a `redact()` projection, which has no answer key. Both are on the documented exam path — use `evaluate()` there, which returns `deferred` / `unscorable` instead of throwing.
22
23
  - **`evaluate(data, response)` → `ItemOutcome`** — the resilient result: `scored`, `deferred` (graded later by an AI or a human), `graded` (the grade came back), or `unscorable`. "Not graded yet" is expressible in the type system and is never conflated with a zero.
@@ -114,8 +115,8 @@ result.passed; // boolean, or null while provisional. Never record a provision
114
115
  | `@intellectif/lk-core/xapi` | `xAPIBuilder`, `XAPIVerb`, `validateXAPIStatement` |
115
116
 
116
117
  The barrel re-exports everything, and it is the **only** entry point for attempt
117
- plans, attempt state, item groups, redaction, the type registry and content
118
- hashing — those have no subpath of their own.
118
+ plans, attempt state, item groups, redaction, the type registry, authoring drafts
119
+ and content hashing — those have no subpath of their own.
119
120
 
120
121
  All exports ship as ESM + CJS with `.d.ts` types. Tree-shakeable; `sideEffects: false`. Node >= 20. In browsers, the SDK's own code needs `Object.hasOwn` — Chrome and Edge 93, Firefox 92, Safari 15.4 — because output targets ES2022 and nothing is polyfilled.
121
122
  Zod is the single runtime dependency: the package depends on `zod@^3.25` and imports the **Zod 4 API** from its `zod/v4` subpath, so it coexists with an app still on Zod 3.
@@ -125,7 +126,7 @@ Zod is the single runtime dependency: the package depends on `zod@^3.25` and imp
125
126
  - [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md) — start here on any upgrade from 0.3.x, 0.4.x or 0.5.x.
126
127
  - [Changelog](https://github.com/intellectif/learning-kit/blob/main/packages/lk-core/CHANGELOG.md) — every release, with the reasoning.
127
128
  - [Grade-stability vectors](https://github.com/intellectif/learning-kit/blob/main/packages/lk-core/vectors/README.md) — the package's grading frozen as data; replay it against the build you install.
128
- - [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md) — data model, validation, fetch → validate → render flow.
129
+ - [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md) — data model, validation, fetch → validate → render flow, and building an editor (the draft issue codes).
129
130
  - [Project README](https://github.com/intellectif/learning-kit#readme) — the full picture, including the React renderers.
130
131
  - [Contributing](https://github.com/intellectif/learning-kit/blob/main/CONTRIBUTING.md) — adding a new activity type.
131
132
 
@@ -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
- /** Display text shown to the learner. */
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 XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIStatement as O, type XAPIVerbObject as P, levenshteinDistance as Q, matchText as R, type ScoringDetail as S, type TextMatchPolicy as T, type ValidationError as V, type WrittenResponseData as W, type XAPIActor as X, type ActivityDataMap as a, type ActivityFeedback as b, type ActivityMedia as c, type ActivityResult as d, type ActivityType as e, type FillInTheBlanksLearnerResponse as f, type Grader as g, type GraderKind as h, type GraderUsage as i, type GradingState as j, type InteractionEvent as k, type InteractionKind as l, type ItemOutcome as m, type LearnerResponseMap as n, type MultipleChoiceLearnerResponse as o, type MultipleChoiceOption as p, type ScoringOutcome as q, type ScoringResult as r, type TextMatchResult as s, type ValidationResult as t, type WrittenResponseLearnerResponse as u, type WrittenResponseRubric as v, type WrittenResponseRubricCriterion as w, type XAPIConfig as x, type XAPIContext as y, type XAPIContextActivities as z };
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
- /** Display text shown to the learner. */
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 XAPIError as E, type FillInTheBlanksData as F, type GradeRecord as G, type XAPIObject as H, type InlineCorrection as I, type XAPIResult as J, type XAPIScore as K, type LearnerResponse as L, type MultipleChoiceData as M, type NativeControlHint as N, type XAPIStatement as O, type XAPIVerbObject as P, levenshteinDistance as Q, matchText as R, type ScoringDetail as S, type TextMatchPolicy as T, type ValidationError as V, type WrittenResponseData as W, type XAPIActor as X, type ActivityDataMap as a, type ActivityFeedback as b, type ActivityMedia as c, type ActivityResult as d, type ActivityType as e, type FillInTheBlanksLearnerResponse as f, type Grader as g, type GraderKind as h, type GraderUsage as i, type GradingState as j, type InteractionEvent as k, type InteractionKind as l, type ItemOutcome as m, type LearnerResponseMap as n, type MultipleChoiceLearnerResponse as o, type MultipleChoiceOption as p, type ScoringOutcome as q, type ScoringResult as r, type TextMatchResult as s, type ValidationResult as t, type WrittenResponseLearnerResponse as u, type WrittenResponseRubric as v, type WrittenResponseRubricCriterion as w, type XAPIConfig as x, type XAPIContext as y, type XAPIContextActivities as z };
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
- var _chunkFKH4YT5Xcjs = require('./chunk-FKH4YT5X.cjs');
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: _chunkFKH4YT5Xcjs.MediaSchema.optional(),
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: _chunkFKH4YT5Xcjs.RedactedMediaSchema.optional(),
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 = _chunkFKH4YT5Xcjs.getActivityTypeDescriptor.call(void 0, item.type);
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 multipleChoiceJsonSchema = _v4.z.toJSONSchema(_chunkFKH4YT5Xcjs.MultipleChoiceDataSchema, {
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(_chunkFKH4YT5Xcjs.FillInTheBlanksDataSchema, {
150
+ var fillInTheBlanksJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.FillInTheBlanksDataSchema, {
147
151
  target: "draft-7"
148
152
  });
149
- var writtenResponseJsonSchema = _v4.z.toJSONSchema(_chunkFKH4YT5Xcjs.WrittenResponseDataSchema, {
153
+ var writtenResponseJsonSchema = _v4.z.toJSONSchema(_chunk7ODQRNR4cjs.WrittenResponseDataSchema, {
150
154
  target: "draft-7"
151
155
  });
152
156
  function jsonSchemaFor(type) {
153
- const descriptor = _chunkFKH4YT5Xcjs.getActivityTypeDescriptor.call(void 0, type);
157
+ const descriptor = _chunk7ODQRNR4cjs.getActivityTypeDescriptor.call(void 0, type);
154
158
  if (descriptor === void 0) {
155
- throw new (0, _chunkFKH4YT5Xcjs.UnknownActivityTypeError)(type);
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 = _chunkFKH4YT5Xcjs.getActivityTypeDescriptor.call(void 0, type);
166
+ const descriptor = _chunk7ODQRNR4cjs.getActivityTypeDescriptor.call(void 0, type);
163
167
  if (descriptor === void 0) {
164
- throw new (0, _chunkFKH4YT5Xcjs.UnknownActivityTypeError)(String(type));
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
- exports.StimulusSchema = StimulusSchema; exports.ItemGroupSchema = ItemGroupSchema; exports.RedactedStimulusSchema = RedactedStimulusSchema; exports.RedactedItemGroupSchema = RedactedItemGroupSchema; exports.validateItemGroup = validateItemGroup; exports.stimulusJsonSchema = stimulusJsonSchema; exports.itemGroupJsonSchema = itemGroupJsonSchema; exports.multipleChoiceJsonSchema = multipleChoiceJsonSchema; exports.fillInTheBlanksJsonSchema = fillInTheBlanksJsonSchema; exports.writtenResponseJsonSchema = writtenResponseJsonSchema; exports.jsonSchemaFor = jsonSchemaFor; exports.validateActivity = validateActivity;
194
- //# sourceMappingURL=chunk-FVLKIL6W.cjs.map
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