@intellectif/lk-core 1.12.0 → 1.13.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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/ai-check.cjs +2 -2
  4. package/dist/ai-check.d.cts +1 -1
  5. package/dist/ai-check.d.ts +1 -1
  6. package/dist/ai-check.js +2 -2
  7. package/dist/{ai-Cu17iVHa.d.ts → ai-hC5pGYeu.d.cts} +122 -5
  8. package/dist/ai-hC5pGYeu.d.cts.map +1 -0
  9. package/dist/{ai-Cu17iVHa.d.cts → ai-hC5pGYeu.d.ts} +122 -5
  10. package/dist/ai-hC5pGYeu.d.ts.map +1 -0
  11. package/dist/{ai-writing-gO4GRscd.cjs → ai-writing-BDSYuifR.cjs} +18 -5
  12. package/dist/ai-writing-BDSYuifR.cjs.map +1 -0
  13. package/dist/{ai-writing-BgeeCOl7.js → ai-writing-Dsmvv4T-.js} +18 -5
  14. package/dist/ai-writing-Dsmvv4T-.js.map +1 -0
  15. package/dist/{index-CpamCGyZ.d.cts → index-4x0auNOv.d.cts} +2 -2
  16. package/dist/{index-CpamCGyZ.d.cts.map → index-4x0auNOv.d.cts.map} +1 -1
  17. package/dist/{index-DjZ1vIVg.d.cts → index-BlJU10O4.d.cts} +26 -4
  18. package/dist/index-BlJU10O4.d.cts.map +1 -0
  19. package/dist/{index-BfUyl6ml.d.ts → index-DVW_N4pJ.d.ts} +26 -4
  20. package/dist/index-DVW_N4pJ.d.ts.map +1 -0
  21. package/dist/{index-BTZWUwOe.d.ts → index-Dlei-7PX.d.ts} +2 -2
  22. package/dist/{index-BTZWUwOe.d.ts.map → index-Dlei-7PX.d.ts.map} +1 -1
  23. package/dist/{index-DYnacygI.d.cts → index-DohEHH10.d.cts} +2 -2
  24. package/dist/{index-DYnacygI.d.cts.map → index-DohEHH10.d.cts.map} +1 -1
  25. package/dist/{index-B1zEN1Ur.d.ts → index-Du9Kx_H6.d.ts} +2 -2
  26. package/dist/{index-B1zEN1Ur.d.ts.map → index-Du9Kx_H6.d.ts.map} +1 -1
  27. package/dist/index.cjs +11 -7
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +14 -13
  30. package/dist/index.d.cts.map +1 -1
  31. package/dist/index.d.ts +14 -13
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +11 -7
  34. package/dist/index.js.map +1 -1
  35. package/dist/{item-group-CRZNJ08j.cjs → item-group-CqdByaLR.cjs} +54 -22
  36. package/dist/item-group-CqdByaLR.cjs.map +1 -0
  37. package/dist/{item-group-CnlBLM9Z.js → item-group-vTcNy2JF.js} +54 -22
  38. package/dist/item-group-vTcNy2JF.js.map +1 -0
  39. package/dist/{registry-BClnM5V3.js → registry-Cn2it-y7.js} +387 -3
  40. package/dist/registry-Cn2it-y7.js.map +1 -0
  41. package/dist/{registry-DgLcT4rZ.cjs → registry-DUTE1vtq.cjs} +387 -3
  42. package/dist/registry-DUTE1vtq.cjs.map +1 -0
  43. package/dist/{schemas-BA8Y8z-S.cjs → schemas-BrA6CFWz.cjs} +3 -3
  44. package/dist/{schemas-BA8Y8z-S.cjs.map → schemas-BrA6CFWz.cjs.map} +1 -1
  45. package/dist/{schemas-VZnN68qG.js → schemas-DlxeS7Lu.js} +3 -3
  46. package/dist/{schemas-VZnN68qG.js.map → schemas-DlxeS7Lu.js.map} +1 -1
  47. package/dist/schemas.cjs +3 -3
  48. package/dist/schemas.d.cts +2 -2
  49. package/dist/schemas.d.ts +2 -2
  50. package/dist/schemas.js +3 -3
  51. package/dist/{scoring-Cfu_2qQ1.cjs → scoring-IjLi2fYF.cjs} +2 -2
  52. package/dist/{scoring-Cfu_2qQ1.cjs.map → scoring-IjLi2fYF.cjs.map} +1 -1
  53. package/dist/{scoring-BxDiwN7I.js → scoring-weQ4SzwB.js} +2 -2
  54. package/dist/{scoring-BxDiwN7I.js.map → scoring-weQ4SzwB.js.map} +1 -1
  55. package/dist/scoring.cjs +2 -2
  56. package/dist/scoring.d.cts +2 -2
  57. package/dist/scoring.d.ts +2 -2
  58. package/dist/scoring.js +2 -2
  59. package/dist/{xapi-BhROEuZQ.cjs → xapi-BqEbpWag.cjs} +2 -2
  60. package/dist/{xapi-BhROEuZQ.cjs.map → xapi-BqEbpWag.cjs.map} +1 -1
  61. package/dist/{xapi-BitXmAYB.js → xapi-DbRVH_RB.js} +2 -2
  62. package/dist/{xapi-BitXmAYB.js.map → xapi-DbRVH_RB.js.map} +1 -1
  63. package/dist/xapi.cjs +1 -1
  64. package/dist/xapi.d.cts +1 -1
  65. package/dist/xapi.d.ts +1 -1
  66. package/dist/xapi.js +1 -1
  67. package/package.json +1 -1
  68. package/vectors/scoring.json +934 -1
  69. package/vectors/validation.json +827 -1
  70. package/dist/ai-Cu17iVHa.d.cts.map +0 -1
  71. package/dist/ai-Cu17iVHa.d.ts.map +0 -1
  72. package/dist/ai-writing-BgeeCOl7.js.map +0 -1
  73. package/dist/ai-writing-gO4GRscd.cjs.map +0 -1
  74. package/dist/index-BfUyl6ml.d.ts.map +0 -1
  75. package/dist/index-DjZ1vIVg.d.cts.map +0 -1
  76. package/dist/item-group-CRZNJ08j.cjs.map +0 -1
  77. package/dist/item-group-CnlBLM9Z.js.map +0 -1
  78. package/dist/registry-BClnM5V3.js.map +0 -1
  79. package/dist/registry-DgLcT4rZ.cjs.map +0 -1
@@ -442,7 +442,8 @@ const BUILT_IN_ACTIVITY_TYPES = [
442
442
  "gap-select",
443
443
  "dictation",
444
444
  "read-aloud",
445
- "speaking-response"
445
+ "speaking-response",
446
+ "matching"
446
447
  ];
447
448
  /** Whether `type` names an activity type the SDK ships. */
448
449
  function isBuiltInActivityType(type) {
@@ -2327,6 +2328,20 @@ const DRAFT_ISSUE_SEVERITY = {
2327
2328
  gs_choice_text_required: "incomplete",
2328
2329
  gs_correct_choice_required: "incomplete",
2329
2330
  gs_correct_choice_unknown: "invalid",
2331
+ mt_prompts_required: "incomplete",
2332
+ mt_prompt_id_required: "invalid",
2333
+ mt_prompt_id_duplicate: "invalid",
2334
+ mt_prompt_text_required: "incomplete",
2335
+ mt_options_too_few: "incomplete",
2336
+ mt_option_id_required: "invalid",
2337
+ mt_option_id_duplicate: "invalid",
2338
+ mt_option_text_required: "incomplete",
2339
+ mt_option_label_required: "incomplete",
2340
+ mt_option_label_duplicate: "invalid",
2341
+ mt_option_use_invalid: "invalid",
2342
+ mt_correct_option_required: "incomplete",
2343
+ mt_correct_option_unknown: "invalid",
2344
+ mt_option_reused: "invalid",
2330
2345
  wr_prompt_required: "incomplete",
2331
2346
  wr_min_words_required: "incomplete",
2332
2347
  wr_min_words_invalid: "invalid",
@@ -3086,6 +3101,10 @@ const ITEM_FINDING_SEVERITY = {
3086
3101
  gs_choices_duplicate: "warning",
3087
3102
  gs_bank_no_distractor: "warning",
3088
3103
  gs_answer_in_passage: "warning",
3104
+ mt_title_reveals_answer: "warning",
3105
+ mt_options_duplicate: "warning",
3106
+ mt_answer_in_prompt: "warning",
3107
+ mt_no_distractor: "warning",
3089
3108
  wr_criterion_name_duplicate: "warning",
3090
3109
  wr_levels_unreadable: "warning",
3091
3110
  ra_text_long_for_time: "warning",
@@ -3356,6 +3375,40 @@ function critiqueGapSelect(draft) {
3356
3375
  });
3357
3376
  return findings;
3358
3377
  }
3378
+ /** Whether `text` writes out `answer` as whole words — an answer of one short word never counts. */
3379
+ function writesOut(text, answer) {
3380
+ const needle = fold(answer);
3381
+ return needle.length > 0 && !isShort(needle) && containsWords(fold(text), needle);
3382
+ }
3383
+ function critiqueMatching(draft) {
3384
+ const findings = [];
3385
+ const prompts = listOf(draft.prompts);
3386
+ const options = listOf(draft.options);
3387
+ const optionText = (option) => isRecord(option) ? written(option.text) : void 0;
3388
+ const answerOf = (prompt) => isRecord(prompt) ? optionText(options.find((option) => isRecord(option) && option.id === prompt.correctOptionId)) : void 0;
3389
+ const title = written(draft.title);
3390
+ if (title !== void 0 && prompts.some((prompt) => {
3391
+ const answer = answerOf(prompt);
3392
+ return answer !== void 0 && writesOut(title, answer);
3393
+ })) findings.push(finding("mt_title_reveals_answer", ["title"], "The title, which the learner sees, contains the answer to a prompt."));
3394
+ for (const { first, second } of duplicates(options, optionText)) findings.push(finding("mt_options_duplicate", [
3395
+ "options",
3396
+ second,
3397
+ "text"
3398
+ ], `Options ${ordinal(first)} and ${ordinal(second)} read the same, so a learner cannot tell them apart.`));
3399
+ prompts.forEach((prompt, index) => {
3400
+ const text = isRecord(prompt) ? written(prompt.text) : void 0;
3401
+ const answer = answerOf(prompt);
3402
+ if (text !== void 0 && answer !== void 0 && writesOut(text, answer)) findings.push(finding("mt_answer_in_prompt", [
3403
+ "prompts",
3404
+ index,
3405
+ "text"
3406
+ ], `Prompt ${ordinal(index)} contains "${answer}", the option that answers it, word for word.`));
3407
+ });
3408
+ const promptCount = prompts.filter(isRecord).length;
3409
+ if (draft.optionUse === "once" && promptCount >= 2 && options.length <= promptCount) findings.push(finding("mt_no_distractor", ["options"], `Each option is used once, and there are ${options.length} options for ${promptCount} prompts, so the last prompt is answered by elimination. Add an option that answers no prompt.`));
3410
+ return findings;
3411
+ }
3359
3412
  /** Why a criterion's levels cannot be graded on, said to the author. */
3360
3413
  const LEVELS_PROBLEM = {
3361
3414
  "not-a-list": (criterion) => `Criterion ${criterion}'s levels are not a list`,
@@ -3745,6 +3798,219 @@ function pairsOneToOne(placeholders, gaps) {
3745
3798
  if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) return false;
3746
3799
  return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
3747
3800
  }
3801
+ // src/schemas/matching.ts
3802
+ /** Zod schema for one option of the shared list. Loose: a consumer sidecar survives validation. */
3803
+ const MatchingOptionSchema = z.looseObject({
3804
+ id: z.string().min(1),
3805
+ label: z.string().min(1).optional(),
3806
+ text: z.string().min(1)
3807
+ });
3808
+ /** Zod schema for one prompt. Whether its `correctOptionId` exists is checked on the activity. */
3809
+ const MatchingPromptSchema = z.looseObject({
3810
+ id: z.string().min(1),
3811
+ text: z.string().min(1),
3812
+ correctOptionId: z.string().min(1),
3813
+ feedback: z.string().optional()
3814
+ });
3815
+ /** Labels compared as a learner reads them: `"A"`, `" a "` and `"a"` are one key. */
3816
+ function labelKey(label) {
3817
+ return label.trim().toLowerCase();
3818
+ }
3819
+ /** Whether every element of `values` is distinct. */
3820
+ const distinct = (values) => new Set(values).size === values.length;
3821
+ /**
3822
+ * Zod schema validating the full Matching data contract.
3823
+ *
3824
+ * Five semantic guards, each an authoring error that would otherwise reach a
3825
+ * learner:
3826
+ *
3827
+ * 1. **Prompt ids are unique** — the scorer looks a match up by prompt id, so a
3828
+ * duplicate makes one prompt unscoreable.
3829
+ * 2. **Option ids are unique** — an answer key naming an id two options share
3830
+ * means two right answers nobody wrote.
3831
+ * 3. **Option labels are unique**, ignoring case and surrounding space — two
3832
+ * options both labelled "C" cannot be told apart by the key the learner
3833
+ * answers with.
3834
+ * 4. **Every `correctOptionId` names an option.** An answer key pointing at an
3835
+ * option nobody can pick is a prompt that cannot be passed.
3836
+ * 5. **Under `optionUse: 'once'`, no option answers two prompts.** Otherwise
3837
+ * the item says each option is used once while its own key uses one twice.
3838
+ */
3839
+ const MatchingDataSchema = z.looseObject({
3840
+ schemaVersion: z.literal("1.0"),
3841
+ type: z.literal("matching"),
3842
+ id: z.string().min(1),
3843
+ title: z.string().min(1),
3844
+ instructions: z.string().optional(),
3845
+ optionsTitle: z.string().optional(),
3846
+ prompts: z.array(MatchingPromptSchema).min(1),
3847
+ options: z.array(MatchingOptionSchema).min(2),
3848
+ optionUse: z.enum(["once", "many"]).optional(),
3849
+ scoringStrategy: z.enum(["all-or-nothing", "partial"]),
3850
+ shuffleOptions: z.boolean().optional(),
3851
+ media: MediaSchema.optional(),
3852
+ feedback: FeedbackSchema.optional(),
3853
+ passThreshold: z.number().min(0).max(1).optional(),
3854
+ locale: z.string().optional(),
3855
+ learningObjectives: z.array(z.string()).optional(),
3856
+ difficultyLevel: z.literal([
3857
+ 1,
3858
+ 2,
3859
+ 3,
3860
+ 4,
3861
+ 5
3862
+ ]).optional(),
3863
+ ai: AiPermissionsSchema.optional()
3864
+ }).refine((data) => distinct(data.prompts.map((prompt) => prompt.id)), {
3865
+ error: "Prompt ids must be unique.",
3866
+ path: ["prompts"]
3867
+ }).refine((data) => distinct(data.options.map((option) => option.id)), {
3868
+ error: "Option ids must be unique.",
3869
+ path: ["options"]
3870
+ }).refine((data) => distinct(data.options.map((option) => option.label).filter((label) => label !== void 0).map(labelKey)), {
3871
+ error: "Option labels must be unique.",
3872
+ path: ["options"]
3873
+ }).refine((data) => {
3874
+ const ids = new Set(data.options.map((option) => option.id));
3875
+ return data.prompts.every((prompt) => ids.has(prompt.correctOptionId));
3876
+ }, {
3877
+ error: "Each prompt needs a `correctOptionId` that is one of the options.",
3878
+ path: ["prompts"]
3879
+ }).refine((data) => data.optionUse !== "once" || distinct(data.prompts.map((prompt) => prompt.correctOptionId)), {
3880
+ error: "With `optionUse: \"once\"`, no option may be the answer to more than one prompt.",
3881
+ path: ["prompts"]
3882
+ });
3883
+ // src/authoring/matching.ts
3884
+ /** Mirrors `MatchingDataSchema`: `options.min(2)`. */
3885
+ const MIN_OPTIONS$1 = 2;
3886
+ const OPTION_USES = ["once", "many"];
3887
+ /**
3888
+ * Draft support for `matching`.
3889
+ *
3890
+ * A new draft has no prompts and no options, and `scoringStrategy` starts at
3891
+ * `all-or-nothing`, as on every built-in: partial credit is the author's to
3892
+ * give — though an exam's matching task is marked prompt by prompt, so one
3893
+ * copied from a paper wants `partial`.
3894
+ *
3895
+ * As in gap-select, no answer is pre-selected: a prompt's `correctOptionId`
3896
+ * starts unwritten, and the draft stays `incomplete` until someone says which
3897
+ * option answers each prompt.
3898
+ */
3899
+ const matchingAuthoring = {
3900
+ createDraft: ({ newId }) => ({
3901
+ schemaVersion: "1.0",
3902
+ type: "matching",
3903
+ id: newId(),
3904
+ title: "",
3905
+ prompts: [],
3906
+ options: [],
3907
+ scoringStrategy: "all-or-nothing"
3908
+ }),
3909
+ checkDraft: checkMatchingDraft,
3910
+ critique: critiqueMatching
3911
+ };
3912
+ function checkMatchingDraft(draft) {
3913
+ const issues = checkIdentity(draft, "matching");
3914
+ issues.push(...checkScoringStrategy(draft));
3915
+ if (!isUnset(draft.optionUse) && !OPTION_USES.includes(draft.optionUse)) issues.push(issue("mt_option_use_invalid", ["optionUse"], "Say whether each option is used \"once\" or \"many\" times, or leave it out for \"many\"."));
3916
+ const options = isUnset(draft.options) ? [] : draft.options;
3917
+ const optionIds = /* @__PURE__ */ new Set();
3918
+ if (Array.isArray(options)) issues.push(...checkOptions(options, optionIds));
3919
+ const prompts = isUnset(draft.prompts) ? [] : draft.prompts;
3920
+ if (Array.isArray(prompts)) issues.push(...checkPrompts(prompts, optionIds, draft.optionUse === "once"));
3921
+ issues.push(...checkSharedOptional(draft));
3922
+ return issues;
3923
+ }
3924
+ /**
3925
+ * The shared list. Its uniqueness rules are reported at `options`, where the
3926
+ * schema reports them; what is missing from one option, at that option.
3927
+ */
3928
+ function checkOptions(options, optionIds) {
3929
+ const issues = [];
3930
+ const atOptions = [];
3931
+ if (options.length < MIN_OPTIONS$1) issues.push(issue("mt_options_too_few", ["options"], `Add at least ${MIN_OPTIONS$1} options to the list the prompts are matched with.`));
3932
+ const repeatedIds = /* @__PURE__ */ new Set();
3933
+ const labels = /* @__PURE__ */ new Map();
3934
+ const repeatedLabels = /* @__PURE__ */ new Set();
3935
+ for (const [index, option] of options.entries()) {
3936
+ if (!isRecord(option)) continue;
3937
+ const ordinal = index + 1;
3938
+ if (isMissingId(option.id)) issues.push(issue("mt_option_id_required", [
3939
+ "options",
3940
+ index,
3941
+ "id"
3942
+ ], `Option ${ordinal} has no id.`));
3943
+ else if (typeof option.id === "string") {
3944
+ if (optionIds.has(option.id)) repeatedIds.add(option.id);
3945
+ optionIds.add(option.id);
3946
+ }
3947
+ if (isUnwritten(option.text)) issues.push(issue("mt_option_text_required", [
3948
+ "options",
3949
+ index,
3950
+ "text"
3951
+ ], `Write the text of option ${ordinal}.`));
3952
+ if (typeof option.label === "string" && option.label.trim() === "") issues.push(issue("mt_option_label_required", [
3953
+ "options",
3954
+ index,
3955
+ "label"
3956
+ ], `Write the label of option ${ordinal}, such as "A" or "iv", or remove it.`));
3957
+ else if (typeof option.label === "string") {
3958
+ const key = labelKey(option.label);
3959
+ if (labels.has(key)) repeatedLabels.add(labels.get(key));
3960
+ else labels.set(key, option.label.trim());
3961
+ }
3962
+ }
3963
+ for (const id of repeatedIds) atOptions.push(issue("mt_option_id_duplicate", ["options"], `More than one option has the id "${id}".`));
3964
+ for (const label of repeatedLabels) atOptions.push(issue("mt_option_label_duplicate", ["options"], `More than one option is labelled "${label}". Learners answer with the label, so each needs its own.`));
3965
+ return [...issues, ...atOptions];
3966
+ }
3967
+ /**
3968
+ * Every prompt. Its uniqueness rule and its answer key against the list are
3969
+ * reported at `prompts`, where the schema reports them; a missing id, text or
3970
+ * key, at the prompt's own field.
3971
+ */
3972
+ function checkPrompts(prompts, optionIds, once) {
3973
+ const issues = [];
3974
+ const atPrompts = [];
3975
+ if (prompts.length === 0) issues.push(issue("mt_prompts_required", ["prompts"], "Add at least one prompt to match with an option."));
3976
+ const seen = /* @__PURE__ */ new Set();
3977
+ const repeated = /* @__PURE__ */ new Set();
3978
+ const answering = /* @__PURE__ */ new Map();
3979
+ for (const [index, prompt] of prompts.entries()) {
3980
+ if (!isRecord(prompt)) continue;
3981
+ const ordinal = index + 1;
3982
+ const name = typeof prompt.id === "string" && prompt.id !== "" ? `"${prompt.id}"` : ordinal;
3983
+ if (isMissingId(prompt.id)) issues.push(issue("mt_prompt_id_required", [
3984
+ "prompts",
3985
+ index,
3986
+ "id"
3987
+ ], `Prompt ${ordinal} has no id.`));
3988
+ else if (typeof prompt.id === "string") {
3989
+ if (seen.has(prompt.id)) repeated.add(prompt.id);
3990
+ seen.add(prompt.id);
3991
+ }
3992
+ if (isUnwritten(prompt.text)) issues.push(issue("mt_prompt_text_required", [
3993
+ "prompts",
3994
+ index,
3995
+ "text"
3996
+ ], `Write the text of prompt ${ordinal}.`));
3997
+ const key = prompt.correctOptionId;
3998
+ if (isUnwritten(key)) issues.push(issue("mt_correct_option_required", [
3999
+ "prompts",
4000
+ index,
4001
+ "correctOptionId"
4002
+ ], `Choose the option that answers prompt ${name}.`));
4003
+ else if (typeof key === "string") {
4004
+ if (!optionIds.has(key)) atPrompts.push(issue("mt_correct_option_unknown", ["prompts"], `Prompt ${name} is answered by "${key}", which is not one of the options.`));
4005
+ answering.set(key, [...answering.get(key) ?? [], String(name)]);
4006
+ }
4007
+ }
4008
+ for (const id of repeated) atPrompts.push(issue("mt_prompt_id_duplicate", ["prompts"], `More than one prompt has the id "${id}".`));
4009
+ if (once) {
4010
+ for (const [optionId, names] of answering) if (names.length > 1) atPrompts.push(issue("mt_option_reused", ["prompts"], `Option "${optionId}" answers prompts ${names.join(" and ")}, but each option is used once. Choose another answer, or let options be used more than once.`));
4011
+ }
4012
+ return [...issues, ...atPrompts];
4013
+ }
3748
4014
  // src/schemas/multiple-choice.ts
3749
4015
  /**
3750
4016
  * Zod schema for a single Multiple Choice option. Loose: unknown keys are
@@ -4946,6 +5212,35 @@ const RedactedGapSelectDataSchema = z.strictObject({
4946
5212
  presentation: z.literal("dropdown").optional(),
4947
5213
  shuffleChoices: z.boolean().optional()
4948
5214
  });
5215
+ /** A redacted matching option: unchanged, because the learner picks from the list. */
5216
+ const RedactedMatchingOptionSchema = z.strictObject({
5217
+ id: z.string().min(1),
5218
+ label: z.string().min(1).optional(),
5219
+ text: z.string().min(1)
5220
+ });
5221
+ /** A redacted prompt: what the learner reads; `correctOptionId` and feedback are gone. */
5222
+ const RedactedMatchingPromptSchema = z.strictObject({
5223
+ id: z.string().min(1),
5224
+ text: z.string().min(1)
5225
+ });
5226
+ /**
5227
+ * Redacted Matching data. As in Gap Select, the list the learner picks from
5228
+ * survives in full and only the key is withheld: each prompt's
5229
+ * `correctOptionId`. `optionUse` survives because it changes what the learner
5230
+ * may do — an exam says "You may use any letter more than once" in its
5231
+ * instructions, and the component moves an option used once. `scoringStrategy`
5232
+ * stays answer-key, as on every type.
5233
+ */
5234
+ const RedactedMatchingDataSchema = z.strictObject({
5235
+ ...redactedBase,
5236
+ type: z.literal("matching"),
5237
+ instructions: z.string().optional(),
5238
+ optionsTitle: z.string().optional(),
5239
+ prompts: z.array(RedactedMatchingPromptSchema).min(1),
5240
+ options: z.array(RedactedMatchingOptionSchema).min(2),
5241
+ optionUse: z.enum(["once", "many"]).optional(),
5242
+ shuffleOptions: z.boolean().optional()
5243
+ });
4949
5244
  /**
4950
5245
  * Redacted Written Response data: the prompt, word bounds and rubric are
4951
5246
  * learner-visible (a rubric tells the learner what they are graded on);
@@ -5185,6 +5480,48 @@ function scoreGapSelect(data, response) {
5185
5480
  details
5186
5481
  };
5187
5482
  }
5483
+ // src/scoring/activity-scorers/matching.ts
5484
+ /**
5485
+ * Scores a Matching response.
5486
+ *
5487
+ * Each prompt is an identity comparison between the option the learner matched
5488
+ * it with and `correctOptionId`, as a gap select's gap is. A prompt with no
5489
+ * match, or matched with an id that is not an option (a tampered payload, or
5490
+ * content edited after the attempt), is scored incorrect and reported as
5491
+ * `incorrect-omission`, so an unanswered prompt stays distinguishable from a
5492
+ * wrong one downstream.
5493
+ *
5494
+ * Under `optionUse: 'once'` a response that uses one option twice is scored
5495
+ * prompt by prompt like any other: the SDK's component never produces one, and
5496
+ * a second use earns nothing the key does not give it, since the key uses each
5497
+ * option at most once.
5498
+ */
5499
+ function scoreMatching(data, response) {
5500
+ const optionIds = new Set(data.options.map((option) => option.id));
5501
+ const details = [];
5502
+ const perPromptCorrect = [];
5503
+ for (const prompt of data.prompts) {
5504
+ const raw = response.matches[prompt.id];
5505
+ const matched = typeof raw === "string" ? raw : "";
5506
+ const answered = matched !== "" && optionIds.has(matched);
5507
+ const correct = answered && matched === prompt.correctOptionId;
5508
+ perPromptCorrect.push(correct);
5509
+ details.push({
5510
+ itemId: prompt.id,
5511
+ outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
5512
+ learnerResponse: [matched],
5513
+ correctResponse: [prompt.correctOptionId],
5514
+ weight: 1
5515
+ });
5516
+ }
5517
+ const correctPrompts = perPromptCorrect.filter(Boolean).length;
5518
+ return {
5519
+ score: data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perPromptCorrect) : partialBlankStrategy(correctPrompts, data.prompts.length),
5520
+ maxScore: 1,
5521
+ feedback: null,
5522
+ details
5523
+ };
5524
+ }
5188
5525
  // src/scoring/activity-scorers/multiple-choice.ts
5189
5526
  /**
5190
5527
  * Scores a Multiple Choice response. Pure; trusts its typed inputs (validation
@@ -5447,6 +5784,32 @@ const GAP_SELECT_FIELD_POLICY = {
5447
5784
  feedback: "answer-key"
5448
5785
  }
5449
5786
  };
5787
+ /**
5788
+ * Matching: as in Gap Select, the list the learner picks from is public, key
5789
+ * by key, and each prompt's `correctOptionId` is the one field withheld.
5790
+ * `optionUse` is public because it changes what the learner may do: the
5791
+ * component moves an option used once, and the paper says so.
5792
+ */
5793
+ const MATCHING_FIELD_POLICY = {
5794
+ ...SHARED_PUBLIC_FIELDS,
5795
+ instructions: "public",
5796
+ optionsTitle: "public",
5797
+ optionUse: "public",
5798
+ shuffleOptions: "public",
5799
+ scoringStrategy: "answer-key",
5800
+ feedback: FEEDBACK_FIELD_POLICY,
5801
+ options: {
5802
+ id: "public",
5803
+ label: "public",
5804
+ text: "public"
5805
+ },
5806
+ prompts: {
5807
+ id: "public",
5808
+ text: "public",
5809
+ correctOptionId: "answer-key",
5810
+ feedback: "answer-key"
5811
+ }
5812
+ };
5450
5813
  const FILL_IN_THE_BLANKS_FIELD_POLICY = {
5451
5814
  ...SHARED_PUBLIC_FIELDS,
5452
5815
  instructions: "public",
@@ -5627,6 +5990,26 @@ const gapSelectType = defineActivityType({
5627
5990
  authoring: gapSelectAuthoring,
5628
5991
  mediaSlots: () => MAIN_MEDIA
5629
5992
  });
5993
+ /** Built-in Matching descriptor. */
5994
+ const matchingType = defineActivityType({
5995
+ type: "matching",
5996
+ schema: MatchingDataSchema,
5997
+ scoring: {
5998
+ kind: "sync",
5999
+ score: scoreMatching
6000
+ },
6001
+ isAnswered: (response) => Object.values(response?.matches ?? {}).some((optionId) => optionId !== ""),
6002
+ fieldPolicy: MATCHING_FIELD_POLICY,
6003
+ redactedSchema: RedactedMatchingDataSchema,
6004
+ interop: {
6005
+ xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
6006
+ xapiInteractionType: "matching",
6007
+ correctResponsesPattern: (data) => [data.prompts.map((prompt) => `${prompt.id}[.]${prompt.correctOptionId}`).join("[,]")]
6008
+ },
6009
+ interactions: ["option-matched", "submitted"],
6010
+ authoring: matchingAuthoring,
6011
+ mediaSlots: () => MAIN_MEDIA
6012
+ });
5630
6013
  /**
5631
6014
  * Built-in Dictation descriptor. Graded synchronously: the score is the
5632
6015
  * character-level similarity of the typed text to the transcript, a pure
@@ -5803,7 +6186,8 @@ const BUILT_IN_DESCRIPTORS = {
5803
6186
  ],
5804
6187
  authoring: speakingResponseAuthoring,
5805
6188
  mediaSlots: () => MAIN_MEDIA
5806
- })
6189
+ }),
6190
+ matching: matchingType
5807
6191
  };
5808
6192
  // src/schemas/read-schema.ts
5809
6193
  /**
@@ -5969,4 +6353,4 @@ function registeredActivityTypes() {
5969
6353
  }
5970
6354
  export { GROUP_CAPTIONS_REVEAL_DICTATION as $, keyPositionFinding as A, SCRIPT_MAX_TURNS as At, countWords as B, READ_ALOUD_MAX_REFERENCE_LENGTH as C, passageWithoutPlaceholders as Ct, SPEECH_ASSESSMENT_MAX_WORDS as D, PLACEHOLDER_RE as Dt, SPEECH_ASSESSMENT_MAX_TEXT_LENGTH as E, FillInTheBlanksDataSchema as Et, matchText as F, isMissingId as G, checkMedia as H, LEVEL_PROBABILITY_TOLERANCE as I, isUnwritten as J, isRecord as K, levelDistribution as L, normaliseFinding as M, maxUnits as Mt, policyFor as N, MultipleChoiceDataSchema as O, MediaSchema as Ot, levenshteinDistance as P, DictationDataSchema as Q, levelKey as R, READ_ALOUD_MAX_DIMENSION_WEIGHT as S, numberedPassage as St, READ_ALOUD_MAX_TAKES as T, revealsAnswer as Tt, coveredPathsOf as U, DRAFT_ISSUE_SEVERITY as V, covers as W, pathKey as X, issue as Y, refusesEmpty as Z, GapSelectDataSchema as _, BUILT_IN_ACTIVITY_TYPES as _t, readSchema as a, STEP as at, ReadAloudDataSchema as b, containsWords as bt, dictationType as c, dictationReferenceWords as ct, multipleChoiceType as d, DICTATION_MAX_EQUIVALENCE_LENGTH as dt, groupCaptionsField as et, readAloudType as f, DICTATION_MAX_TEXT_LENGTH as ft, WrittenResponseDataSchema as g, withValidationScope as gt, defineActivityType as h, normalizeDictationText as ht, isOwnSchema as i, UnknownActivityTypeError as it, finding as j, ScriptSchema as jt, MultipleChoiceOptionMediaSchema as k, RedactedMediaSchema as kt, fillInTheBlanksType as l, DICTATION_MAX_ACCEPTED_TRANSCRIPTS as lt, guardOf as m, codePointLength as mt, registerActivityType as n, DeferredScoringError as nt, standardJsonSchemaOf as o, alignDictation as ot, writtenResponseType as p, DICTATION_MAX_TRANSCRIPT_LENGTH as pt, isUnset as q, registeredActivityTypes as r, RedactedScoringError as rt, MEDIA_FIELD_POLICY as s, alignSequences as st, getActivityTypeDescriptor as t, ActivitySchemaError as tt, gapSelectType as u, DICTATION_MAX_EQUIVALENCES as ut, SPEAKING_MEASURES as v, everyBuiltInTypeHandled as vt, READ_ALOUD_MAX_SECONDS as w, placeholderOrder as wt, CANONICAL_LOCALE_RE as x, fold as xt, SpeakingResponseDataSchema as y, isBuiltInActivityType as yt, readRubricLevels as z };
5971
6355
 
5972
- //# sourceMappingURL=registry-BClnM5V3.js.map
6356
+ //# sourceMappingURL=registry-Cn2it-y7.js.map