@intellectif/lk-core 1.6.0 → 1.8.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 (86) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/{ai-i3jwXnpX.d.ts → ai-Cnm0qE8v.d.cts} +334 -16
  3. package/dist/ai-Cnm0qE8v.d.cts.map +1 -0
  4. package/dist/{ai-i3jwXnpX.d.cts → ai-Cnm0qE8v.d.ts} +334 -16
  5. package/dist/ai-Cnm0qE8v.d.ts.map +1 -0
  6. package/dist/ai-check.cjs +88 -7
  7. package/dist/ai-check.cjs.map +1 -1
  8. package/dist/ai-check.d.cts +11 -7
  9. package/dist/ai-check.d.cts.map +1 -1
  10. package/dist/ai-check.d.ts +11 -7
  11. package/dist/ai-check.d.ts.map +1 -1
  12. package/dist/ai-check.js +88 -7
  13. package/dist/ai-check.js.map +1 -1
  14. package/dist/{ai-writing-C4YRVrFi.cjs → ai-writing-Bh2Y53cY.cjs} +2567 -203
  15. package/dist/ai-writing-Bh2Y53cY.cjs.map +1 -0
  16. package/dist/{ai-writing-OJ5m_1EB.js → ai-writing-DeIV-GfN.js} +2428 -172
  17. package/dist/ai-writing-DeIV-GfN.js.map +1 -0
  18. package/dist/{index-Dj92T76o.d.ts → index-CtoPhJAH.d.ts} +2 -8
  19. package/dist/index-CtoPhJAH.d.ts.map +1 -0
  20. package/dist/{index--U7hHV0s.d.cts → index-DMh3OYSb.d.cts} +2 -8
  21. package/dist/index-DMh3OYSb.d.cts.map +1 -0
  22. package/dist/{index-WBHSrIUU.d.ts → index-HXvg0YRQ.d.ts} +5 -12
  23. package/dist/index-HXvg0YRQ.d.ts.map +1 -0
  24. package/dist/{index-CFRgZLhH.d.cts → index-fZ67k-WR.d.cts} +5 -12
  25. package/dist/index-fZ67k-WR.d.cts.map +1 -0
  26. package/dist/{index-DeD8EI1D.d.ts → index-g-EWIugp.d.ts} +2 -42
  27. package/dist/index-g-EWIugp.d.ts.map +1 -0
  28. package/dist/{index-Uh1u57yq.d.cts → index-oST8GBFa.d.cts} +2 -42
  29. package/dist/index-oST8GBFa.d.cts.map +1 -0
  30. package/dist/index.cjs +287 -20
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +227 -71
  33. package/dist/index.d.cts.map +1 -1
  34. package/dist/index.d.ts +227 -71
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +276 -21
  37. package/dist/index.js.map +1 -1
  38. package/dist/{item-group-BbvvH_Ob.js → item-group-BceUoxoM.js} +4 -6
  39. package/dist/{item-group-BbvvH_Ob.js.map → item-group-BceUoxoM.js.map} +1 -1
  40. package/dist/{item-group-CQRbb8Rx.cjs → item-group-KV4hYu3a.cjs} +4 -6
  41. package/dist/{item-group-CQRbb8Rx.cjs.map → item-group-KV4hYu3a.cjs.map} +1 -1
  42. package/dist/{registry-Ih5n4PVZ.cjs → registry-Cymq7Ah0.cjs} +97 -92
  43. package/dist/registry-Cymq7Ah0.cjs.map +1 -0
  44. package/dist/{registry-B23o9UWh.js → registry-tkacvJUI.js} +79 -91
  45. package/dist/registry-tkacvJUI.js.map +1 -0
  46. package/dist/{schemas-yId-bJhD.js → schemas-DM8sN_f0.js} +5 -7
  47. package/dist/{schemas-yId-bJhD.js.map → schemas-DM8sN_f0.js.map} +1 -1
  48. package/dist/{schemas-Q2dyOgc2.cjs → schemas-wDzbKdKa.cjs} +5 -7
  49. package/dist/{schemas-Q2dyOgc2.cjs.map → schemas-wDzbKdKa.cjs.map} +1 -1
  50. package/dist/schemas.cjs +3 -3
  51. package/dist/schemas.d.cts +1 -1
  52. package/dist/schemas.d.ts +1 -1
  53. package/dist/schemas.js +3 -3
  54. package/dist/{scoring-CaPS6q8b.cjs → scoring-D9tFvPEG.cjs} +22 -42
  55. package/dist/{scoring-CaPS6q8b.cjs.map → scoring-D9tFvPEG.cjs.map} +1 -1
  56. package/dist/{scoring-CLMdk8i9.js → scoring-DIBmo_cy.js} +22 -42
  57. package/dist/{scoring-CLMdk8i9.js.map → scoring-DIBmo_cy.js.map} +1 -1
  58. package/dist/scoring.cjs +2 -2
  59. package/dist/scoring.d.cts +2 -2
  60. package/dist/scoring.d.ts +2 -2
  61. package/dist/scoring.js +2 -2
  62. package/dist/{xapi-BX0hvRRB.cjs → xapi-Dyqcl3JK.cjs} +5 -8
  63. package/dist/{xapi-BX0hvRRB.cjs.map → xapi-Dyqcl3JK.cjs.map} +1 -1
  64. package/dist/{xapi-Cdk9fTvV.js → xapi-gCAo549Q.js} +5 -8
  65. package/dist/{xapi-Cdk9fTvV.js.map → xapi-gCAo549Q.js.map} +1 -1
  66. package/dist/xapi.cjs +1 -1
  67. package/dist/xapi.d.cts +1 -1
  68. package/dist/xapi.d.ts +1 -1
  69. package/dist/xapi.js +1 -1
  70. package/package.json +2 -2
  71. package/vectors/README.md +23 -0
  72. package/vectors/variants.d.mts +20 -0
  73. package/vectors/variants.json +5753 -0
  74. package/vectors/variants.mjs +67 -0
  75. package/dist/ai-i3jwXnpX.d.cts.map +0 -1
  76. package/dist/ai-i3jwXnpX.d.ts.map +0 -1
  77. package/dist/ai-writing-C4YRVrFi.cjs.map +0 -1
  78. package/dist/ai-writing-OJ5m_1EB.js.map +0 -1
  79. package/dist/index--U7hHV0s.d.cts.map +0 -1
  80. package/dist/index-CFRgZLhH.d.cts.map +0 -1
  81. package/dist/index-DeD8EI1D.d.ts.map +0 -1
  82. package/dist/index-Dj92T76o.d.ts.map +0 -1
  83. package/dist/index-Uh1u57yq.d.cts.map +0 -1
  84. package/dist/index-WBHSrIUU.d.ts.map +0 -1
  85. package/dist/registry-B23o9UWh.js.map +0 -1
  86. package/dist/registry-Ih5n4PVZ.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- //#region \0rolldown/runtime.js
1
+ // \0rolldown/runtime.js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -19,11 +19,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
19
  value: mod,
20
20
  enumerable: true
21
21
  }) : target, mod));
22
- //#endregion
23
22
  let zod_v4 = require("zod/v4");
24
23
  zod_v4 = __toESM(zod_v4, 1);
25
24
  let zod_v4_core = require("zod/v4/core");
26
- //#region src/schemas/ai.ts
25
+ // src/schemas/ai.ts
27
26
  /**
28
27
  * What an author allows AI to do for one item: see `ActivityAiPermissions`.
29
28
  * Loose like every content schema, so a key a later release adds survives an
@@ -33,8 +32,7 @@ const AiPermissionsSchema = zod_v4.looseObject({
33
32
  explanations: zod_v4.boolean().optional(),
34
33
  hints: zod_v4.boolean().optional()
35
34
  });
36
- //#endregion
37
- //#region src/schemas/feedback.ts
35
+ // src/schemas/feedback.ts
38
36
  /**
39
37
  * Optional authored "overall feedback" shown after submission, selected by
40
38
  * whether the learner passed (h5p-style overall feedback). Both fields are
@@ -44,8 +42,7 @@ const FeedbackSchema = zod_v4.looseObject({
44
42
  correct: zod_v4.string().min(1).optional(),
45
43
  incorrect: zod_v4.string().min(1).optional()
46
44
  });
47
- //#endregion
48
- //#region src/schemas/text-length.ts
45
+ // src/schemas/text-length.ts
49
46
  /**
50
47
  * `schema`, refusing a string longer than `max` UTF-16 code units — its
51
48
  * JavaScript `length`, the unit these limits have always counted in.
@@ -68,8 +65,7 @@ function maxUnits(schema, max) {
68
65
  });
69
66
  }).meta({ maxLength: max });
70
67
  }
71
- //#endregion
72
- //#region src/schemas/media.ts
68
+ // src/schemas/media.ts
73
69
  /**
74
70
  * Optional media attached to an activity, rendered above the question or
75
71
  * passage — e.g. a recording to listen to, or an embedded video to watch
@@ -267,8 +263,7 @@ const RedactedMediaSchema = zod_v4.strictObject({
267
263
  error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds — the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
268
264
  path: ["url"]
269
265
  }).refine(posterFits, POSTER_FITS).refine(tracksFit, TRACKS_FIT).refine(tracksAreDistinct, TRACKS_DISTINCT);
270
- //#endregion
271
- //#region src/schemas/fill-in-the-blanks.ts
266
+ // src/schemas/fill-in-the-blanks.ts
272
267
  /**
273
268
  * Matches `{{ blank_id }}` placeholders in a passage, capturing the trimmed id.
274
269
  * Exported for the draft checks in `authoring/`, which must read a passage
@@ -276,6 +271,12 @@ const RedactedMediaSchema = zod_v4.strictObject({
276
271
  */
277
272
  const PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
278
273
  /**
274
+ * The instructions of an item with a passage: plain text, with no `{{id}}`
275
+ * placeholder. Blanks and gaps live in the passage only, and one written here
276
+ * would be shown to the learner as it stands. Shared by Gap Select.
277
+ */
278
+ const PassageInstructionsSchema = zod_v4.string().refine((text) => text.match(PLACEHOLDER_RE) === null, { error: "The instructions are plain text: a {{id}} placeholder belongs in the passage, where its blank or gap is." });
279
+ /**
279
280
  * Zod schema for a `TextMatchPolicy` — the opt-in matching tolerances a blank
280
281
  * may declare. Every default reproduces the v1 trim + case-fold semantics.
281
282
  */
@@ -323,6 +324,7 @@ const FillInTheBlanksDataSchema = zod_v4.looseObject({
323
324
  type: zod_v4.literal("fill-in-the-blanks"),
324
325
  id: zod_v4.string().min(1),
325
326
  title: zod_v4.string().min(1),
327
+ instructions: PassageInstructionsSchema.optional(),
326
328
  passage: zod_v4.string().min(1),
327
329
  passageHtml: zod_v4.string().optional(),
328
330
  blanks: zod_v4.array(BlankConfigSchema).min(1),
@@ -354,8 +356,7 @@ const FillInTheBlanksDataSchema = zod_v4.looseObject({
354
356
  error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
355
357
  path: ["passage"]
356
358
  });
357
- //#endregion
358
- //#region src/answer-leak.ts
359
+ // src/answer-leak.ts
359
360
  /**
360
361
  * Whether a text gives an answer away: the rule an AI hint is held to, and the
361
362
  * one the item critic holds an author's own hint, title and passage to.
@@ -442,8 +443,7 @@ function revealsAnswer(text, answer, passage, position) {
442
443
  if (before === void 0 && after === void 0) return containsWords(words, needle);
443
444
  return before !== void 0 && containsWords(words, [before, ...needle]) || after !== void 0 && containsWords(words, [...needle, after]);
444
445
  }
445
- //#endregion
446
- //#region src/built-in-types.ts
446
+ // src/built-in-types.ts
447
447
  /**
448
448
  * The activity types the SDK ships, in one closed list.
449
449
  *
@@ -479,8 +479,7 @@ function isBuiltInActivityType(type) {
479
479
  * @internal
480
480
  */
481
481
  function everyBuiltInTypeHandled(_value) {}
482
- //#endregion
483
- //#region src/scoring/dictation/edit-distance.ts
482
+ // src/scoring/dictation/edit-distance.ts
484
483
  /**
485
484
  * The code points of `text`, as numbers. A surrogate pair is one code point; a
486
485
  * lone surrogate iterates as one code point too — deterministic, never thrown.
@@ -532,8 +531,7 @@ function similarity(a, b) {
532
531
  const max = Math.max(codePoints(a).length, codePoints(b).length);
533
532
  return max === 0 ? 0 : (max - editDistance(a, b)) / max;
534
533
  }
535
- //#endregion
536
- //#region src/validation-scope.ts
534
+ // src/validation-scope.ts
537
535
  /**
538
536
  * Work shared for the length of one outermost validation call.
539
537
  *
@@ -572,8 +570,7 @@ function scopeCache(key, create) {
572
570
  }
573
571
  return value;
574
572
  }
575
- //#endregion
576
- //#region src/scoring/dictation/unicode-joining.ts
573
+ // src/scoring/dictation/unicode-joining.ts
577
574
  /**
578
575
  * Unicode data the dictation normaliser reads when it decides whether a joiner
579
576
  * changes how the text is drawn (see `normalize.ts`): the viramas, and which
@@ -791,8 +788,7 @@ const JOINS_BACKWARD = [
791
788
  [69577, 69578],
792
789
  [125184, 125251]
793
790
  ];
794
- //#endregion
795
- //#region src/scoring/dictation/normalize.ts
791
+ // src/scoring/dictation/normalize.ts
796
792
  /**
797
793
  * The longest transcript a dictation accepts, in code points, before and after
798
794
  * its equivalences are applied — and its rules may not grow it past twice
@@ -1752,8 +1748,7 @@ function searchReveal(forms, candidate) {
1752
1748
  return fields.some((field) => containsPhrase(field, phrase));
1753
1749
  });
1754
1750
  }
1755
- //#endregion
1756
- //#region src/scoring/dictation/align.ts
1751
+ // src/scoring/dictation/align.ts
1757
1752
  /**
1758
1753
  * The words the scorer can emit a detail for, one list per candidate — index 0
1759
1754
  * is the transcript, index n is `acceptedTranscripts[n − 1]` — each word in the
@@ -1952,8 +1947,7 @@ function alignWords(reference, attempt) {
1952
1947
  }
1953
1948
  return words;
1954
1949
  }
1955
- //#endregion
1956
- //#region src/errors.ts
1950
+ // src/errors.ts
1957
1951
  /** Thrown when activity data fails schema validation at a component boundary. */
1958
1952
  var ActivitySchemaError = class extends Error {
1959
1953
  activityType;
@@ -2007,8 +2001,7 @@ var DeferredScoringError = class extends Error {
2007
2001
  this.name = "DeferredScoringError";
2008
2002
  }
2009
2003
  };
2010
- //#endregion
2011
- //#region src/schemas/after-issues.ts
2004
+ // src/schemas/after-issues.ts
2012
2005
  /**
2013
2006
  * A check zod runs even when another field was refused. An ordinary check is
2014
2007
  * skipped once any issue has been reported, so an author fixing a recording's
@@ -2045,8 +2038,7 @@ function parsedFields(payload) {
2045
2038
  refused
2046
2039
  };
2047
2040
  }
2048
- //#endregion
2049
- //#region src/schemas/dictation.ts
2041
+ // src/schemas/dictation.ts
2050
2042
  /**
2051
2043
  * Zod schema for the slower second recording of a dictation. Loose like every
2052
2044
  * content schema (a sidecar survives), audio only, and — deliberately — with no
@@ -2228,8 +2220,7 @@ function groupCaptionsField(group, written) {
2228
2220
  }
2229
2221
  /** The message every validator gives for {@link groupCaptionsRevealDictation}. */
2230
2222
  const GROUP_CAPTIONS_REVEAL_DICTATION = "The stimulus recording carries captions, and a dictation in this group plays that recording: the captions are the answer. Remove the captions, or give the dictation its own recording.";
2231
- //#endregion
2232
- //#region src/authoring/issues.ts
2223
+ // src/authoring/issues.ts
2233
2224
  /**
2234
2225
  * Every issue code the SDK's draft checks can report, with its severity.
2235
2226
  *
@@ -2565,8 +2556,7 @@ function coveredPathsOf(reported) {
2565
2556
  }
2566
2557
  return covered;
2567
2558
  }
2568
- //#endregion
2569
- //#region src/authoring/dictation.ts
2559
+ // src/authoring/dictation.ts
2570
2560
  /**
2571
2561
  * Draft support for `dictation`.
2572
2562
  *
@@ -2756,8 +2746,7 @@ function checkTolerance(tolerance) {
2756
2746
  }
2757
2747
  return issues;
2758
2748
  }
2759
- //#endregion
2760
- //#region src/count-words.ts
2749
+ // src/count-words.ts
2761
2750
  /**
2762
2751
  * Canonical word counter for written-response bounds: tokens are
2763
2752
  * maximal runs of non-whitespace (split on `\s+`), so hyphenated forms
@@ -2862,8 +2851,7 @@ function levelDistribution(values, count, chosen) {
2862
2851
  const sum = read.reduce((total, p) => total + p, 0);
2863
2852
  return Math.abs(sum - 1) <= 1e-6 && read[chosen] === Math.max(...read) ? read : null;
2864
2853
  }
2865
- //#endregion
2866
- //#region src/scoring/strategies/all-or-nothing.ts
2854
+ // src/scoring/strategies/all-or-nothing.ts
2867
2855
  /**
2868
2856
  * All-or-nothing scoring: full credit only when every item is correct.
2869
2857
  *
@@ -2873,8 +2861,7 @@ function levelDistribution(values, count, chosen) {
2873
2861
  function allOrNothingStrategy(correctItems) {
2874
2862
  return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
2875
2863
  }
2876
- //#endregion
2877
- //#region src/scoring/strategies/partial.ts
2864
+ // src/scoring/strategies/partial.ts
2878
2865
  /**
2879
2866
  * Partial-credit scoring for Multiple Choice — balanced/symmetric scheme.
2880
2867
  *
@@ -2914,8 +2901,7 @@ function partialStrategy(correctSelected, incorrectSelected, totalCorrect, total
2914
2901
  function partialBlankStrategy(correctBlanks, totalBlanks) {
2915
2902
  return correctBlanks / totalBlanks;
2916
2903
  }
2917
- //#endregion
2918
- //#region src/scoring/text-match.ts
2904
+ // src/scoring/text-match.ts
2919
2905
  /** Combining marks (any script) removed for diacritic folding after NFD decomposition. */
2920
2906
  const COMBINING_MARKS_RE = /\p{M}/gu;
2921
2907
  /**
@@ -3008,8 +2994,7 @@ function matchText(input, accepted, policy = {}) {
3008
2994
  via: "none"
3009
2995
  };
3010
2996
  }
3011
- //#endregion
3012
- //#region src/scoring/activity-scorers/fill-in-the-blanks.ts
2997
+ // src/scoring/activity-scorers/fill-in-the-blanks.ts
3013
2998
  /**
3014
2999
  * Resolves the effective match policy for a blank. The legacy
3015
3000
  * `caseSensitive` / `trimWhitespace` flags map onto the baseline policy
@@ -3053,8 +3038,7 @@ function scoreFillInTheBlanks(data, response) {
3053
3038
  details
3054
3039
  };
3055
3040
  }
3056
- //#endregion
3057
- //#region src/authoring/findings.ts
3041
+ // src/authoring/findings.ts
3058
3042
  /**
3059
3043
  * Every finding code the SDK's item critic can report, with its severity.
3060
3044
  *
@@ -3073,6 +3057,7 @@ const ITEM_FINDING_SEVERITY = {
3073
3057
  fib_hint_reveals_answer: "warning",
3074
3058
  fib_answer_in_passage: "warning",
3075
3059
  fib_accepted_answer_redundant: "advice",
3060
+ fib_tolerance_spans_words: "advice",
3076
3061
  gs_title_reveals_answer: "warning",
3077
3062
  gs_choices_duplicate: "warning",
3078
3063
  gs_bank_no_distractor: "warning",
@@ -3083,7 +3068,12 @@ const ITEM_FINDING_SEVERITY = {
3083
3068
  media_transcript_unused: "advice",
3084
3069
  media_script_differs: "warning",
3085
3070
  media_transcript_differs: "warning",
3086
- set_key_position_same: "warning"
3071
+ set_key_position_same: "warning",
3072
+ set_key_position_bias: "warning",
3073
+ set_key_longest_bias: "warning",
3074
+ set_stem_duplicate: "warning",
3075
+ set_item_answers_item: "warning",
3076
+ set_key_repeated: "advice"
3087
3077
  };
3088
3078
  function finding(code, path, message) {
3089
3079
  return {
@@ -3107,8 +3097,7 @@ function normaliseFinding(found) {
3107
3097
  severity: documented ?? (found.severity === "advice" ? "advice" : "warning")
3108
3098
  };
3109
3099
  }
3110
- //#endregion
3111
- //#region src/authoring/critique.ts
3100
+ // src/authoring/critique.ts
3112
3101
  /**
3113
3102
  * The built-in item critic: what an experienced item writer points out on
3114
3103
  * review, as far as a rule can see it without a model.
@@ -3274,6 +3263,17 @@ function critiqueFillInTheBlanks(draft) {
3274
3263
  laterIndex
3275
3264
  ], `"${laterText}" is already accepted as "${accepted[earlier]}" under this blank's matching.`));
3276
3265
  });
3266
+ const forgiven = policyFor(blank).levenshtein ?? 0;
3267
+ const spanning = accepted.findIndex((answer) => {
3268
+ const text = written(answer);
3269
+ return text !== void 0 && /\s/.test(text.trim());
3270
+ });
3271
+ if (forgiven > 0 && spanning !== -1) findings.push(finding("fib_tolerance_spans_words", [
3272
+ "blanks",
3273
+ index,
3274
+ "acceptedAnswers",
3275
+ spanning
3276
+ ], `Blank ${ordinal(index)} forgives ${forgiven === 1 ? "one edit" : `${forgiven} edits`} over an answer of several words, so "${accepted[spanning]}" with one word in another form is marked right. Make it a one-word blank, or take the tolerance off.`));
3277
3277
  });
3278
3278
  return findings;
3279
3279
  }
@@ -3391,8 +3391,7 @@ function keyPositionFinding(items, path) {
3391
3391
  if (positions.length < KEY_POSITION_MINIMUM || !positions.every((at) => at === positions[0])) return [];
3392
3392
  return [finding("set_key_position_same", path, `In all ${positions.length} single-answer questions the right option is option ${ordinal(positions[0])}. Vary its position, or shuffle the options.`)];
3393
3393
  }
3394
- //#endregion
3395
- //#region src/authoring/fill-in-the-blanks.ts
3394
+ // src/authoring/fill-in-the-blanks.ts
3396
3395
  /**
3397
3396
  * Draft support for `fill-in-the-blanks`.
3398
3397
  *
@@ -3524,8 +3523,7 @@ function pairsOneToOne$1(placeholders, blanks) {
3524
3523
  if (placeholders.size !== blankIds.length) return false;
3525
3524
  return blankIds.every((id) => typeof id === "string" && placeholders.get(id) === 1);
3526
3525
  }
3527
- //#endregion
3528
- //#region src/authoring/gap-select.ts
3526
+ // src/authoring/gap-select.ts
3529
3527
  /** Mirrors `GapSelectChoiceSchema` arrays: `.min(2)` on both a gap's own list and a bank. */
3530
3528
  const MIN_CHOICES = 2;
3531
3529
  /**
@@ -3723,8 +3721,7 @@ function pairsOneToOne(placeholders, gaps) {
3723
3721
  if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) return false;
3724
3722
  return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
3725
3723
  }
3726
- //#endregion
3727
- //#region src/schemas/multiple-choice.ts
3724
+ // src/schemas/multiple-choice.ts
3728
3725
  /**
3729
3726
  * Zod schema for a single Multiple Choice option. Loose: unknown keys are
3730
3727
  * preserved through validation (forward-compat / consumer sidecars — B7).
@@ -3788,6 +3785,7 @@ const MultipleChoiceDataSchema = zod_v4.looseObject({
3788
3785
  type: zod_v4.literal("multiple-choice"),
3789
3786
  id: zod_v4.string().min(1),
3790
3787
  title: zod_v4.string().min(1),
3788
+ instructions: zod_v4.string().optional(),
3791
3789
  question: zod_v4.string().min(1),
3792
3790
  questionHtml: zod_v4.string().optional(),
3793
3791
  mode: zod_v4.enum(["single", "multi"]),
@@ -3817,8 +3815,7 @@ const MultipleChoiceDataSchema = zod_v4.looseObject({
3817
3815
  error: "Option ids must be unique within the activity.",
3818
3816
  path: ["options"]
3819
3817
  });
3820
- //#endregion
3821
- //#region src/authoring/multiple-choice.ts
3818
+ // src/authoring/multiple-choice.ts
3822
3819
  /** Mirrors `MultipleChoiceDataSchema`: `options` is `.min(2).max(26)`. */
3823
3820
  const MIN_OPTIONS = 2;
3824
3821
  const MAX_OPTIONS = 26;
@@ -3921,8 +3918,7 @@ function checkOptionMedia(media, index, ordinal) {
3921
3918
  if (kind === "video" || kind === "embed") return [issue("mc_option_media_kind", [...path, "type"], `Option ${ordinal} carries ${kind === "embed" ? "an embedded player" : "a video"}, which cannot be an option: its controls swallow the click that selects the answer. Use a picture or a recording.`)];
3922
3919
  return checkMedia(media, path, MultipleChoiceOptionMediaSchema);
3923
3920
  }
3924
- //#endregion
3925
- //#region src/scoring/speech/limits.ts
3921
+ // src/scoring/speech/limits.ts
3926
3922
  /**
3927
3923
  * The longest reference text a read-aloud item accepts, in code points, before
3928
3924
  * and after normalisation. It is the dictation transcript cap on purpose: the
@@ -3972,8 +3968,7 @@ const SPEECH_ASSESSMENT_MAX_WORDS = 1e3;
3972
3968
  * read-aloud item comes near it.
3973
3969
  */
3974
3970
  const SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
3975
- //#endregion
3976
- //#region src/scoring/speech/locale.ts
3971
+ // src/scoring/speech/locale.ts
3977
3972
  /**
3978
3973
  * A BCP 47 tag in the one spelling read-aloud accepts: a lowercase language of
3979
3974
  * two or three letters, an optional title-case script, and a region of two
@@ -3989,8 +3984,7 @@ const SPEECH_ASSESSMENT_MAX_TEXT_LENGTH = DICTATION_MAX_TEXT_LENGTH;
3989
3984
  * Internal: the content schema and the assessment validator share it.
3990
3985
  */
3991
3986
  const CANONICAL_LOCALE_RE = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?-(?:[A-Z]{2}|[0-9]{3})$/;
3992
- //#endregion
3993
- //#region src/schemas/read-aloud.ts
3987
+ // src/schemas/read-aloud.ts
3994
3988
  /** The dimensions a read-aloud grade can be made of, in the order they are documented. */
3995
3989
  const READ_ALOUD_DIMENSIONS = [
3996
3990
  "accuracy",
@@ -4143,8 +4137,7 @@ const ReadAloudDataSchema = zod_v4.looseObject({
4143
4137
  if (minSeconds !== void 0 && minSeconds >= maxSeconds) report(["recording", "minSeconds"], minSeconds, "The shortest take must be shorter than the longest.");
4144
4138
  }
4145
4139
  }));
4146
- //#endregion
4147
- //#region src/authoring/read-aloud.ts
4140
+ // src/authoring/read-aloud.ts
4148
4141
  /**
4149
4142
  * Draft support for `read-aloud`.
4150
4143
  *
@@ -4307,8 +4300,7 @@ function checkSlowRecording(draft) {
4307
4300
  if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) issues.push(issue("ra_slow_media_same_recording", ["slowMedia", "url"], "The slow model recording is the same file as the recording. Point it at the slower reading."));
4308
4301
  return issues;
4309
4302
  }
4310
- //#endregion
4311
- //#region src/authoring/written-response.ts
4303
+ // src/authoring/written-response.ts
4312
4304
  /**
4313
4305
  * Draft support for `written-response`.
4314
4306
  *
@@ -4396,8 +4388,7 @@ function checkCriteria(criteria) {
4396
4388
  else if (totalWeight === Number.POSITIVE_INFINITY) issues.push(issue("wr_rubric_weights_too_large", ["rubric", "criteria"], "The rubric weights add up to more than can be calculated with. Use smaller weights."));
4397
4389
  return issues;
4398
4390
  }
4399
- //#endregion
4400
- //#region src/schemas/gap-select.ts
4391
+ // src/schemas/gap-select.ts
4401
4392
  /**
4402
4393
  * Zod schema for one selectable choice. Loose, like every other schema here:
4403
4394
  * unknown keys are preserved so a consumer sidecar survives validation.
@@ -4461,6 +4452,7 @@ const GapSelectDataSchema = zod_v4.looseObject({
4461
4452
  type: zod_v4.literal("gap-select"),
4462
4453
  id: zod_v4.string().min(1),
4463
4454
  title: zod_v4.string().min(1),
4455
+ instructions: PassageInstructionsSchema.optional(),
4464
4456
  passage: zod_v4.string().min(1),
4465
4457
  passageHtml: zod_v4.string().optional(),
4466
4458
  gaps: zod_v4.array(GapSelectGapSchema).min(1),
@@ -4521,8 +4513,7 @@ const GapSelectDataSchema = zod_v4.looseObject({
4521
4513
  error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
4522
4514
  path: ["gaps"]
4523
4515
  });
4524
- //#endregion
4525
- //#region src/schemas/written-response.ts
4516
+ // src/schemas/written-response.ts
4526
4517
  /** Zod schema for a single rubric criterion. Loose: unknown keys preserved. */
4527
4518
  const WrittenResponseRubricCriterionSchema = zod_v4.looseObject({
4528
4519
  name: zod_v4.string().min(1),
@@ -4593,8 +4584,7 @@ const WrittenResponseDataSchema = zod_v4.looseObject({
4593
4584
  error: "maxWords must be greater than or equal to minWords.",
4594
4585
  path: ["maxWords"]
4595
4586
  });
4596
- //#endregion
4597
- //#region src/schemas/redacted.ts
4587
+ // src/schemas/redacted.ts
4598
4588
  /**
4599
4589
  * Schemas for REDACTED activity data — the learner-safe projection `redact()`
4600
4590
  * produces (R7). Deliberately STRICT (`z.strictObject`), the opposite of the
@@ -4658,6 +4648,7 @@ const RedactedMultipleChoiceOptionSchema = zod_v4.strictObject({
4658
4648
  const RedactedMultipleChoiceDataSchema = zod_v4.strictObject({
4659
4649
  ...redactedBase,
4660
4650
  type: zod_v4.literal("multiple-choice"),
4651
+ instructions: zod_v4.string().optional(),
4661
4652
  question: zod_v4.string().min(1),
4662
4653
  questionHtml: zod_v4.string().optional(),
4663
4654
  mode: zod_v4.enum(["single", "multi"]),
@@ -4673,6 +4664,7 @@ const RedactedBlankConfigSchema = zod_v4.strictObject({
4673
4664
  const RedactedFillInTheBlanksDataSchema = zod_v4.strictObject({
4674
4665
  ...redactedBase,
4675
4666
  type: zod_v4.literal("fill-in-the-blanks"),
4667
+ instructions: zod_v4.string().optional(),
4676
4668
  passage: zod_v4.string().min(1),
4677
4669
  passageHtml: zod_v4.string().optional(),
4678
4670
  blanks: zod_v4.array(RedactedBlankConfigSchema).min(1)
@@ -4711,6 +4703,7 @@ const RedactedGapSelectGapSchema = zod_v4.strictObject({
4711
4703
  const RedactedGapSelectDataSchema = zod_v4.strictObject({
4712
4704
  ...redactedBase,
4713
4705
  type: zod_v4.literal("gap-select"),
4706
+ instructions: zod_v4.string().optional(),
4714
4707
  passage: zod_v4.string().min(1),
4715
4708
  passageHtml: zod_v4.string().optional(),
4716
4709
  gaps: zod_v4.array(RedactedGapSelectGapSchema).min(1),
@@ -4836,8 +4829,7 @@ const RedactedReadAloudDataSchema = zod_v4.strictObject({
4836
4829
  path: ["slowMedia", "url"]
4837
4830
  });
4838
4831
  });
4839
- //#endregion
4840
- //#region src/scoring/activity-scorers/dictation.ts
4832
+ // src/scoring/activity-scorers/dictation.ts
4841
4833
  /**
4842
4834
  * Scores a Dictation response: the character-level similarity of what the
4843
4835
  * learner typed to the transcript, over the whole sentence.
@@ -4876,8 +4868,7 @@ function scoreDictation(data, response) {
4876
4868
  details
4877
4869
  };
4878
4870
  }
4879
- //#endregion
4880
- //#region src/scoring/activity-scorers/gap-select.ts
4871
+ // src/scoring/activity-scorers/gap-select.ts
4881
4872
  /**
4882
4873
  * The choices a gap offers: its own list, or the bank it names.
4883
4874
  *
@@ -4931,8 +4922,7 @@ function scoreGapSelect(data, response) {
4931
4922
  details
4932
4923
  };
4933
4924
  }
4934
- //#endregion
4935
- //#region src/scoring/activity-scorers/multiple-choice.ts
4925
+ // src/scoring/activity-scorers/multiple-choice.ts
4936
4926
  /**
4937
4927
  * Scores a Multiple Choice response. Pure; trusts its typed inputs (validation
4938
4928
  * is the component boundary's job). Options are looked up by id; an unknown
@@ -4976,8 +4966,7 @@ function scoreMultipleChoice(data, response) {
4976
4966
  details
4977
4967
  };
4978
4968
  }
4979
- //#endregion
4980
- //#region src/registry/define.ts
4969
+ // src/registry/define.ts
4981
4970
  /**
4982
4971
  * Identity helper that gives full type inference when authoring a descriptor:
4983
4972
  *
@@ -4992,8 +4981,7 @@ function scoreMultipleChoice(data, response) {
4992
4981
  function defineActivityType(descriptor) {
4993
4982
  return descriptor;
4994
4983
  }
4995
- //#endregion
4996
- //#region src/registry/keep-when.ts
4984
+ // src/registry/keep-when.ts
4997
4985
  /**
4998
4986
  * A nested field policy that keeps its field only when the field's value
4999
4987
  * passes a test. Internal: the symbol is never exported, so no host policy can
@@ -5018,8 +5006,7 @@ function keepWhen(policy, test) {
5018
5006
  function guardOf(policy) {
5019
5007
  return policy[KEEP_WHEN];
5020
5008
  }
5021
- //#endregion
5022
- //#region src/registry/builtins.ts
5009
+ // src/registry/builtins.ts
5023
5010
  /**
5024
5011
  * Where the built-in types keep their files (`mediaSlots`). Every type has its
5025
5012
  * `media`; dictation and read-aloud add a slower recording; a multiple-choice
@@ -5137,6 +5124,7 @@ const TEXT_MATCH_FIELD_POLICY = {
5137
5124
  };
5138
5125
  const MULTIPLE_CHOICE_FIELD_POLICY = {
5139
5126
  ...SHARED_PUBLIC_FIELDS,
5127
+ instructions: "public",
5140
5128
  question: "public",
5141
5129
  questionHtml: "public",
5142
5130
  mode: "public",
@@ -5171,6 +5159,7 @@ const MULTIPLE_CHOICE_FIELD_POLICY = {
5171
5159
  */
5172
5160
  const GAP_SELECT_FIELD_POLICY = {
5173
5161
  ...SHARED_PUBLIC_FIELDS,
5162
+ instructions: "public",
5174
5163
  passage: "public",
5175
5164
  passageHtml: "public",
5176
5165
  presentation: "public",
@@ -5197,6 +5186,7 @@ const GAP_SELECT_FIELD_POLICY = {
5197
5186
  };
5198
5187
  const FILL_IN_THE_BLANKS_FIELD_POLICY = {
5199
5188
  ...SHARED_PUBLIC_FIELDS,
5189
+ instructions: "public",
5200
5190
  passage: "public",
5201
5191
  passageHtml: "public",
5202
5192
  scoringStrategy: "answer-key",
@@ -5499,8 +5489,7 @@ const BUILT_IN_DESCRIPTORS = {
5499
5489
  dictation: dictationType,
5500
5490
  "read-aloud": readAloudType
5501
5491
  };
5502
- //#endregion
5503
- //#region src/schemas/read-schema.ts
5492
+ // src/schemas/read-schema.ts
5504
5493
  /**
5505
5494
  * How every entry point reads a schema — `validateActivity`, `validateDraft`,
5506
5495
  * `validateItemGroup`, `redact`, `assertRedacted` — so a schema is read one way
@@ -5616,8 +5605,7 @@ function standardJsonSchemaOf(schema, target = "draft-07") {
5616
5605
  const generated = converter.output({ target });
5617
5606
  return generated !== null && typeof generated === "object" && !Array.isArray(generated) ? generated : void 0;
5618
5607
  }
5619
- //#endregion
5620
- //#region src/registry/registry.ts
5608
+ // src/registry/registry.ts
5621
5609
  /**
5622
5610
  * Module-scoped default registry (deliberate — no registry instances until a
5623
5611
  * second consumer exists), created holding the SDK's own types.
@@ -5663,7 +5651,6 @@ function getActivityTypeDescriptor(type) {
5663
5651
  function registeredActivityTypes() {
5664
5652
  return [...registry.keys()];
5665
5653
  }
5666
- //#endregion
5667
5654
  Object.defineProperty(exports, "ActivitySchemaError", {
5668
5655
  enumerable: true,
5669
5656
  get: function() {
@@ -5778,6 +5765,12 @@ Object.defineProperty(exports, "MultipleChoiceOptionMediaSchema", {
5778
5765
  return MultipleChoiceOptionMediaSchema;
5779
5766
  }
5780
5767
  });
5768
+ Object.defineProperty(exports, "PLACEHOLDER_RE", {
5769
+ enumerable: true,
5770
+ get: function() {
5771
+ return PLACEHOLDER_RE;
5772
+ }
5773
+ });
5781
5774
  Object.defineProperty(exports, "READ_ALOUD_MAX_DIMENSION_WEIGHT", {
5782
5775
  enumerable: true,
5783
5776
  get: function() {
@@ -6084,6 +6077,12 @@ Object.defineProperty(exports, "numberedPassage", {
6084
6077
  return numberedPassage;
6085
6078
  }
6086
6079
  });
6080
+ Object.defineProperty(exports, "passageWithoutPlaceholders", {
6081
+ enumerable: true,
6082
+ get: function() {
6083
+ return passageWithoutPlaceholders;
6084
+ }
6085
+ });
6087
6086
  Object.defineProperty(exports, "pathKey", {
6088
6087
  enumerable: true,
6089
6088
  get: function() {
@@ -6096,6 +6095,12 @@ Object.defineProperty(exports, "placeholderOrder", {
6096
6095
  return placeholderOrder;
6097
6096
  }
6098
6097
  });
6098
+ Object.defineProperty(exports, "policyFor", {
6099
+ enumerable: true,
6100
+ get: function() {
6101
+ return policyFor;
6102
+ }
6103
+ });
6099
6104
  Object.defineProperty(exports, "readAloudType", {
6100
6105
  enumerable: true,
6101
6106
  get: function() {
@@ -6157,4 +6162,4 @@ Object.defineProperty(exports, "writtenResponseType", {
6157
6162
  }
6158
6163
  });
6159
6164
 
6160
- //# sourceMappingURL=registry-Ih5n4PVZ.cjs.map
6165
+ //# sourceMappingURL=registry-Cymq7Ah0.cjs.map