@intellectif/lk-core 1.11.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.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/ai-check.cjs +2 -2
- package/dist/ai-check.d.cts +1 -1
- package/dist/ai-check.d.ts +1 -1
- package/dist/ai-check.js +2 -2
- package/dist/{ai-Cu17iVHa.d.ts → ai-hC5pGYeu.d.cts} +122 -5
- package/dist/ai-hC5pGYeu.d.cts.map +1 -0
- package/dist/{ai-Cu17iVHa.d.cts → ai-hC5pGYeu.d.ts} +122 -5
- package/dist/ai-hC5pGYeu.d.ts.map +1 -0
- package/dist/{ai-writing-C7f6aKNW.cjs → ai-writing-BDSYuifR.cjs} +18 -5
- package/dist/ai-writing-BDSYuifR.cjs.map +1 -0
- package/dist/{ai-writing-Dl1bJQbL.js → ai-writing-Dsmvv4T-.js} +18 -5
- package/dist/ai-writing-Dsmvv4T-.js.map +1 -0
- package/dist/{index-CpamCGyZ.d.cts → index-4x0auNOv.d.cts} +2 -2
- package/dist/{index-CpamCGyZ.d.cts.map → index-4x0auNOv.d.cts.map} +1 -1
- package/dist/{index-DjZ1vIVg.d.cts → index-BlJU10O4.d.cts} +26 -4
- package/dist/index-BlJU10O4.d.cts.map +1 -0
- package/dist/{index-BfUyl6ml.d.ts → index-DVW_N4pJ.d.ts} +26 -4
- package/dist/index-DVW_N4pJ.d.ts.map +1 -0
- package/dist/{index-BTZWUwOe.d.ts → index-Dlei-7PX.d.ts} +2 -2
- package/dist/{index-BTZWUwOe.d.ts.map → index-Dlei-7PX.d.ts.map} +1 -1
- package/dist/{index-DYnacygI.d.cts → index-DohEHH10.d.cts} +2 -2
- package/dist/{index-DYnacygI.d.cts.map → index-DohEHH10.d.cts.map} +1 -1
- package/dist/{index-B1zEN1Ur.d.ts → index-Du9Kx_H6.d.ts} +2 -2
- package/dist/{index-B1zEN1Ur.d.ts.map → index-Du9Kx_H6.d.ts.map} +1 -1
- package/dist/index.cjs +12 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -14
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +28 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -8
- package/dist/index.js.map +1 -1
- package/dist/{item-group-D7M3E7K2.cjs → item-group-CqdByaLR.cjs} +100 -26
- package/dist/item-group-CqdByaLR.cjs.map +1 -0
- package/dist/{item-group-uNAT5EX5.js → item-group-vTcNy2JF.js} +95 -27
- package/dist/item-group-vTcNy2JF.js.map +1 -0
- package/dist/{registry-kTqbi4Lb.js → registry-Cn2it-y7.js} +388 -3
- package/dist/registry-Cn2it-y7.js.map +1 -0
- package/dist/{registry-RPfZZEqi.cjs → registry-DUTE1vtq.cjs} +388 -3
- package/dist/registry-DUTE1vtq.cjs.map +1 -0
- package/dist/{schemas-C4V5wfOS.cjs → schemas-BrA6CFWz.cjs} +3 -3
- package/dist/{schemas-C4V5wfOS.cjs.map → schemas-BrA6CFWz.cjs.map} +1 -1
- package/dist/{schemas-rDgIN4Vu.js → schemas-DlxeS7Lu.js} +3 -3
- package/dist/{schemas-rDgIN4Vu.js.map → schemas-DlxeS7Lu.js.map} +1 -1
- package/dist/schemas.cjs +3 -3
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +3 -3
- package/dist/{scoring-KpYfR0lO.cjs → scoring-IjLi2fYF.cjs} +2 -2
- package/dist/{scoring-KpYfR0lO.cjs.map → scoring-IjLi2fYF.cjs.map} +1 -1
- package/dist/{scoring-DshU9i7e.js → scoring-weQ4SzwB.js} +2 -2
- package/dist/{scoring-DshU9i7e.js.map → scoring-weQ4SzwB.js.map} +1 -1
- package/dist/scoring.cjs +2 -2
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/{xapi-CaM7f1sD.cjs → xapi-BqEbpWag.cjs} +2 -2
- package/dist/{xapi-CaM7f1sD.cjs.map → xapi-BqEbpWag.cjs.map} +1 -1
- package/dist/{xapi-fFq9KEXG.js → xapi-DbRVH_RB.js} +2 -2
- package/dist/{xapi-fFq9KEXG.js.map → xapi-DbRVH_RB.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -1
- package/package.json +1 -1
- package/vectors/scoring.json +934 -1
- package/vectors/validation.json +827 -1
- package/dist/ai-Cu17iVHa.d.cts.map +0 -1
- package/dist/ai-Cu17iVHa.d.ts.map +0 -1
- package/dist/ai-writing-C7f6aKNW.cjs.map +0 -1
- package/dist/ai-writing-Dl1bJQbL.js.map +0 -1
- package/dist/index-BfUyl6ml.d.ts.map +0 -1
- package/dist/index-DjZ1vIVg.d.cts.map +0 -1
- package/dist/item-group-D7M3E7K2.cjs.map +0 -1
- package/dist/item-group-uNAT5EX5.js.map +0 -1
- package/dist/registry-RPfZZEqi.cjs.map +0 -1
- package/dist/registry-kTqbi4Lb.js.map +0 -1
|
@@ -464,7 +464,8 @@ const BUILT_IN_ACTIVITY_TYPES = [
|
|
|
464
464
|
"gap-select",
|
|
465
465
|
"dictation",
|
|
466
466
|
"read-aloud",
|
|
467
|
-
"speaking-response"
|
|
467
|
+
"speaking-response",
|
|
468
|
+
"matching"
|
|
468
469
|
];
|
|
469
470
|
/** Whether `type` names an activity type the SDK ships. */
|
|
470
471
|
function isBuiltInActivityType(type) {
|
|
@@ -2304,6 +2305,7 @@ const DRAFT_ISSUE_SEVERITY = {
|
|
|
2304
2305
|
ig_route_stimulus_kind: "invalid",
|
|
2305
2306
|
ig_route_body_required: "incomplete",
|
|
2306
2307
|
ig_route_with_timeline: "invalid",
|
|
2308
|
+
ig_route_quiz_mixed: "invalid",
|
|
2307
2309
|
ig_route_shuffle: "invalid",
|
|
2308
2310
|
ig_route_item_type: "invalid",
|
|
2309
2311
|
ig_route_question_item_unknown: "invalid",
|
|
@@ -2348,6 +2350,20 @@ const DRAFT_ISSUE_SEVERITY = {
|
|
|
2348
2350
|
gs_choice_text_required: "incomplete",
|
|
2349
2351
|
gs_correct_choice_required: "incomplete",
|
|
2350
2352
|
gs_correct_choice_unknown: "invalid",
|
|
2353
|
+
mt_prompts_required: "incomplete",
|
|
2354
|
+
mt_prompt_id_required: "invalid",
|
|
2355
|
+
mt_prompt_id_duplicate: "invalid",
|
|
2356
|
+
mt_prompt_text_required: "incomplete",
|
|
2357
|
+
mt_options_too_few: "incomplete",
|
|
2358
|
+
mt_option_id_required: "invalid",
|
|
2359
|
+
mt_option_id_duplicate: "invalid",
|
|
2360
|
+
mt_option_text_required: "incomplete",
|
|
2361
|
+
mt_option_label_required: "incomplete",
|
|
2362
|
+
mt_option_label_duplicate: "invalid",
|
|
2363
|
+
mt_option_use_invalid: "invalid",
|
|
2364
|
+
mt_correct_option_required: "incomplete",
|
|
2365
|
+
mt_correct_option_unknown: "invalid",
|
|
2366
|
+
mt_option_reused: "invalid",
|
|
2351
2367
|
wr_prompt_required: "incomplete",
|
|
2352
2368
|
wr_min_words_required: "incomplete",
|
|
2353
2369
|
wr_min_words_invalid: "invalid",
|
|
@@ -3107,6 +3123,10 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3107
3123
|
gs_choices_duplicate: "warning",
|
|
3108
3124
|
gs_bank_no_distractor: "warning",
|
|
3109
3125
|
gs_answer_in_passage: "warning",
|
|
3126
|
+
mt_title_reveals_answer: "warning",
|
|
3127
|
+
mt_options_duplicate: "warning",
|
|
3128
|
+
mt_answer_in_prompt: "warning",
|
|
3129
|
+
mt_no_distractor: "warning",
|
|
3110
3130
|
wr_criterion_name_duplicate: "warning",
|
|
3111
3131
|
wr_levels_unreadable: "warning",
|
|
3112
3132
|
ra_text_long_for_time: "warning",
|
|
@@ -3377,6 +3397,40 @@ function critiqueGapSelect(draft) {
|
|
|
3377
3397
|
});
|
|
3378
3398
|
return findings;
|
|
3379
3399
|
}
|
|
3400
|
+
/** Whether `text` writes out `answer` as whole words — an answer of one short word never counts. */
|
|
3401
|
+
function writesOut(text, answer) {
|
|
3402
|
+
const needle = fold(answer);
|
|
3403
|
+
return needle.length > 0 && !isShort(needle) && containsWords(fold(text), needle);
|
|
3404
|
+
}
|
|
3405
|
+
function critiqueMatching(draft) {
|
|
3406
|
+
const findings = [];
|
|
3407
|
+
const prompts = listOf(draft.prompts);
|
|
3408
|
+
const options = listOf(draft.options);
|
|
3409
|
+
const optionText = (option) => isRecord(option) ? written(option.text) : void 0;
|
|
3410
|
+
const answerOf = (prompt) => isRecord(prompt) ? optionText(options.find((option) => isRecord(option) && option.id === prompt.correctOptionId)) : void 0;
|
|
3411
|
+
const title = written(draft.title);
|
|
3412
|
+
if (title !== void 0 && prompts.some((prompt) => {
|
|
3413
|
+
const answer = answerOf(prompt);
|
|
3414
|
+
return answer !== void 0 && writesOut(title, answer);
|
|
3415
|
+
})) findings.push(finding("mt_title_reveals_answer", ["title"], "The title, which the learner sees, contains the answer to a prompt."));
|
|
3416
|
+
for (const { first, second } of duplicates(options, optionText)) findings.push(finding("mt_options_duplicate", [
|
|
3417
|
+
"options",
|
|
3418
|
+
second,
|
|
3419
|
+
"text"
|
|
3420
|
+
], `Options ${ordinal(first)} and ${ordinal(second)} read the same, so a learner cannot tell them apart.`));
|
|
3421
|
+
prompts.forEach((prompt, index) => {
|
|
3422
|
+
const text = isRecord(prompt) ? written(prompt.text) : void 0;
|
|
3423
|
+
const answer = answerOf(prompt);
|
|
3424
|
+
if (text !== void 0 && answer !== void 0 && writesOut(text, answer)) findings.push(finding("mt_answer_in_prompt", [
|
|
3425
|
+
"prompts",
|
|
3426
|
+
index,
|
|
3427
|
+
"text"
|
|
3428
|
+
], `Prompt ${ordinal(index)} contains "${answer}", the option that answers it, word for word.`));
|
|
3429
|
+
});
|
|
3430
|
+
const promptCount = prompts.filter(isRecord).length;
|
|
3431
|
+
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.`));
|
|
3432
|
+
return findings;
|
|
3433
|
+
}
|
|
3380
3434
|
/** Why a criterion's levels cannot be graded on, said to the author. */
|
|
3381
3435
|
const LEVELS_PROBLEM = {
|
|
3382
3436
|
"not-a-list": (criterion) => `Criterion ${criterion}'s levels are not a list`,
|
|
@@ -3766,6 +3820,219 @@ function pairsOneToOne(placeholders, gaps) {
|
|
|
3766
3820
|
if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) return false;
|
|
3767
3821
|
return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
|
|
3768
3822
|
}
|
|
3823
|
+
// src/schemas/matching.ts
|
|
3824
|
+
/** Zod schema for one option of the shared list. Loose: a consumer sidecar survives validation. */
|
|
3825
|
+
const MatchingOptionSchema = zod_v4.looseObject({
|
|
3826
|
+
id: zod_v4.string().min(1),
|
|
3827
|
+
label: zod_v4.string().min(1).optional(),
|
|
3828
|
+
text: zod_v4.string().min(1)
|
|
3829
|
+
});
|
|
3830
|
+
/** Zod schema for one prompt. Whether its `correctOptionId` exists is checked on the activity. */
|
|
3831
|
+
const MatchingPromptSchema = zod_v4.looseObject({
|
|
3832
|
+
id: zod_v4.string().min(1),
|
|
3833
|
+
text: zod_v4.string().min(1),
|
|
3834
|
+
correctOptionId: zod_v4.string().min(1),
|
|
3835
|
+
feedback: zod_v4.string().optional()
|
|
3836
|
+
});
|
|
3837
|
+
/** Labels compared as a learner reads them: `"A"`, `" a "` and `"a"` are one key. */
|
|
3838
|
+
function labelKey(label) {
|
|
3839
|
+
return label.trim().toLowerCase();
|
|
3840
|
+
}
|
|
3841
|
+
/** Whether every element of `values` is distinct. */
|
|
3842
|
+
const distinct = (values) => new Set(values).size === values.length;
|
|
3843
|
+
/**
|
|
3844
|
+
* Zod schema validating the full Matching data contract.
|
|
3845
|
+
*
|
|
3846
|
+
* Five semantic guards, each an authoring error that would otherwise reach a
|
|
3847
|
+
* learner:
|
|
3848
|
+
*
|
|
3849
|
+
* 1. **Prompt ids are unique** — the scorer looks a match up by prompt id, so a
|
|
3850
|
+
* duplicate makes one prompt unscoreable.
|
|
3851
|
+
* 2. **Option ids are unique** — an answer key naming an id two options share
|
|
3852
|
+
* means two right answers nobody wrote.
|
|
3853
|
+
* 3. **Option labels are unique**, ignoring case and surrounding space — two
|
|
3854
|
+
* options both labelled "C" cannot be told apart by the key the learner
|
|
3855
|
+
* answers with.
|
|
3856
|
+
* 4. **Every `correctOptionId` names an option.** An answer key pointing at an
|
|
3857
|
+
* option nobody can pick is a prompt that cannot be passed.
|
|
3858
|
+
* 5. **Under `optionUse: 'once'`, no option answers two prompts.** Otherwise
|
|
3859
|
+
* the item says each option is used once while its own key uses one twice.
|
|
3860
|
+
*/
|
|
3861
|
+
const MatchingDataSchema = zod_v4.looseObject({
|
|
3862
|
+
schemaVersion: zod_v4.literal("1.0"),
|
|
3863
|
+
type: zod_v4.literal("matching"),
|
|
3864
|
+
id: zod_v4.string().min(1),
|
|
3865
|
+
title: zod_v4.string().min(1),
|
|
3866
|
+
instructions: zod_v4.string().optional(),
|
|
3867
|
+
optionsTitle: zod_v4.string().optional(),
|
|
3868
|
+
prompts: zod_v4.array(MatchingPromptSchema).min(1),
|
|
3869
|
+
options: zod_v4.array(MatchingOptionSchema).min(2),
|
|
3870
|
+
optionUse: zod_v4.enum(["once", "many"]).optional(),
|
|
3871
|
+
scoringStrategy: zod_v4.enum(["all-or-nothing", "partial"]),
|
|
3872
|
+
shuffleOptions: zod_v4.boolean().optional(),
|
|
3873
|
+
media: MediaSchema.optional(),
|
|
3874
|
+
feedback: FeedbackSchema.optional(),
|
|
3875
|
+
passThreshold: zod_v4.number().min(0).max(1).optional(),
|
|
3876
|
+
locale: zod_v4.string().optional(),
|
|
3877
|
+
learningObjectives: zod_v4.array(zod_v4.string()).optional(),
|
|
3878
|
+
difficultyLevel: zod_v4.literal([
|
|
3879
|
+
1,
|
|
3880
|
+
2,
|
|
3881
|
+
3,
|
|
3882
|
+
4,
|
|
3883
|
+
5
|
|
3884
|
+
]).optional(),
|
|
3885
|
+
ai: AiPermissionsSchema.optional()
|
|
3886
|
+
}).refine((data) => distinct(data.prompts.map((prompt) => prompt.id)), {
|
|
3887
|
+
error: "Prompt ids must be unique.",
|
|
3888
|
+
path: ["prompts"]
|
|
3889
|
+
}).refine((data) => distinct(data.options.map((option) => option.id)), {
|
|
3890
|
+
error: "Option ids must be unique.",
|
|
3891
|
+
path: ["options"]
|
|
3892
|
+
}).refine((data) => distinct(data.options.map((option) => option.label).filter((label) => label !== void 0).map(labelKey)), {
|
|
3893
|
+
error: "Option labels must be unique.",
|
|
3894
|
+
path: ["options"]
|
|
3895
|
+
}).refine((data) => {
|
|
3896
|
+
const ids = new Set(data.options.map((option) => option.id));
|
|
3897
|
+
return data.prompts.every((prompt) => ids.has(prompt.correctOptionId));
|
|
3898
|
+
}, {
|
|
3899
|
+
error: "Each prompt needs a `correctOptionId` that is one of the options.",
|
|
3900
|
+
path: ["prompts"]
|
|
3901
|
+
}).refine((data) => data.optionUse !== "once" || distinct(data.prompts.map((prompt) => prompt.correctOptionId)), {
|
|
3902
|
+
error: "With `optionUse: \"once\"`, no option may be the answer to more than one prompt.",
|
|
3903
|
+
path: ["prompts"]
|
|
3904
|
+
});
|
|
3905
|
+
// src/authoring/matching.ts
|
|
3906
|
+
/** Mirrors `MatchingDataSchema`: `options.min(2)`. */
|
|
3907
|
+
const MIN_OPTIONS$1 = 2;
|
|
3908
|
+
const OPTION_USES = ["once", "many"];
|
|
3909
|
+
/**
|
|
3910
|
+
* Draft support for `matching`.
|
|
3911
|
+
*
|
|
3912
|
+
* A new draft has no prompts and no options, and `scoringStrategy` starts at
|
|
3913
|
+
* `all-or-nothing`, as on every built-in: partial credit is the author's to
|
|
3914
|
+
* give — though an exam's matching task is marked prompt by prompt, so one
|
|
3915
|
+
* copied from a paper wants `partial`.
|
|
3916
|
+
*
|
|
3917
|
+
* As in gap-select, no answer is pre-selected: a prompt's `correctOptionId`
|
|
3918
|
+
* starts unwritten, and the draft stays `incomplete` until someone says which
|
|
3919
|
+
* option answers each prompt.
|
|
3920
|
+
*/
|
|
3921
|
+
const matchingAuthoring = {
|
|
3922
|
+
createDraft: ({ newId }) => ({
|
|
3923
|
+
schemaVersion: "1.0",
|
|
3924
|
+
type: "matching",
|
|
3925
|
+
id: newId(),
|
|
3926
|
+
title: "",
|
|
3927
|
+
prompts: [],
|
|
3928
|
+
options: [],
|
|
3929
|
+
scoringStrategy: "all-or-nothing"
|
|
3930
|
+
}),
|
|
3931
|
+
checkDraft: checkMatchingDraft,
|
|
3932
|
+
critique: critiqueMatching
|
|
3933
|
+
};
|
|
3934
|
+
function checkMatchingDraft(draft) {
|
|
3935
|
+
const issues = checkIdentity(draft, "matching");
|
|
3936
|
+
issues.push(...checkScoringStrategy(draft));
|
|
3937
|
+
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\"."));
|
|
3938
|
+
const options = isUnset(draft.options) ? [] : draft.options;
|
|
3939
|
+
const optionIds = /* @__PURE__ */ new Set();
|
|
3940
|
+
if (Array.isArray(options)) issues.push(...checkOptions(options, optionIds));
|
|
3941
|
+
const prompts = isUnset(draft.prompts) ? [] : draft.prompts;
|
|
3942
|
+
if (Array.isArray(prompts)) issues.push(...checkPrompts(prompts, optionIds, draft.optionUse === "once"));
|
|
3943
|
+
issues.push(...checkSharedOptional(draft));
|
|
3944
|
+
return issues;
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* The shared list. Its uniqueness rules are reported at `options`, where the
|
|
3948
|
+
* schema reports them; what is missing from one option, at that option.
|
|
3949
|
+
*/
|
|
3950
|
+
function checkOptions(options, optionIds) {
|
|
3951
|
+
const issues = [];
|
|
3952
|
+
const atOptions = [];
|
|
3953
|
+
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.`));
|
|
3954
|
+
const repeatedIds = /* @__PURE__ */ new Set();
|
|
3955
|
+
const labels = /* @__PURE__ */ new Map();
|
|
3956
|
+
const repeatedLabels = /* @__PURE__ */ new Set();
|
|
3957
|
+
for (const [index, option] of options.entries()) {
|
|
3958
|
+
if (!isRecord(option)) continue;
|
|
3959
|
+
const ordinal = index + 1;
|
|
3960
|
+
if (isMissingId(option.id)) issues.push(issue("mt_option_id_required", [
|
|
3961
|
+
"options",
|
|
3962
|
+
index,
|
|
3963
|
+
"id"
|
|
3964
|
+
], `Option ${ordinal} has no id.`));
|
|
3965
|
+
else if (typeof option.id === "string") {
|
|
3966
|
+
if (optionIds.has(option.id)) repeatedIds.add(option.id);
|
|
3967
|
+
optionIds.add(option.id);
|
|
3968
|
+
}
|
|
3969
|
+
if (isUnwritten(option.text)) issues.push(issue("mt_option_text_required", [
|
|
3970
|
+
"options",
|
|
3971
|
+
index,
|
|
3972
|
+
"text"
|
|
3973
|
+
], `Write the text of option ${ordinal}.`));
|
|
3974
|
+
if (typeof option.label === "string" && option.label.trim() === "") issues.push(issue("mt_option_label_required", [
|
|
3975
|
+
"options",
|
|
3976
|
+
index,
|
|
3977
|
+
"label"
|
|
3978
|
+
], `Write the label of option ${ordinal}, such as "A" or "iv", or remove it.`));
|
|
3979
|
+
else if (typeof option.label === "string") {
|
|
3980
|
+
const key = labelKey(option.label);
|
|
3981
|
+
if (labels.has(key)) repeatedLabels.add(labels.get(key));
|
|
3982
|
+
else labels.set(key, option.label.trim());
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
for (const id of repeatedIds) atOptions.push(issue("mt_option_id_duplicate", ["options"], `More than one option has the id "${id}".`));
|
|
3986
|
+
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.`));
|
|
3987
|
+
return [...issues, ...atOptions];
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Every prompt. Its uniqueness rule and its answer key against the list are
|
|
3991
|
+
* reported at `prompts`, where the schema reports them; a missing id, text or
|
|
3992
|
+
* key, at the prompt's own field.
|
|
3993
|
+
*/
|
|
3994
|
+
function checkPrompts(prompts, optionIds, once) {
|
|
3995
|
+
const issues = [];
|
|
3996
|
+
const atPrompts = [];
|
|
3997
|
+
if (prompts.length === 0) issues.push(issue("mt_prompts_required", ["prompts"], "Add at least one prompt to match with an option."));
|
|
3998
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3999
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
4000
|
+
const answering = /* @__PURE__ */ new Map();
|
|
4001
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
4002
|
+
if (!isRecord(prompt)) continue;
|
|
4003
|
+
const ordinal = index + 1;
|
|
4004
|
+
const name = typeof prompt.id === "string" && prompt.id !== "" ? `"${prompt.id}"` : ordinal;
|
|
4005
|
+
if (isMissingId(prompt.id)) issues.push(issue("mt_prompt_id_required", [
|
|
4006
|
+
"prompts",
|
|
4007
|
+
index,
|
|
4008
|
+
"id"
|
|
4009
|
+
], `Prompt ${ordinal} has no id.`));
|
|
4010
|
+
else if (typeof prompt.id === "string") {
|
|
4011
|
+
if (seen.has(prompt.id)) repeated.add(prompt.id);
|
|
4012
|
+
seen.add(prompt.id);
|
|
4013
|
+
}
|
|
4014
|
+
if (isUnwritten(prompt.text)) issues.push(issue("mt_prompt_text_required", [
|
|
4015
|
+
"prompts",
|
|
4016
|
+
index,
|
|
4017
|
+
"text"
|
|
4018
|
+
], `Write the text of prompt ${ordinal}.`));
|
|
4019
|
+
const key = prompt.correctOptionId;
|
|
4020
|
+
if (isUnwritten(key)) issues.push(issue("mt_correct_option_required", [
|
|
4021
|
+
"prompts",
|
|
4022
|
+
index,
|
|
4023
|
+
"correctOptionId"
|
|
4024
|
+
], `Choose the option that answers prompt ${name}.`));
|
|
4025
|
+
else if (typeof key === "string") {
|
|
4026
|
+
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.`));
|
|
4027
|
+
answering.set(key, [...answering.get(key) ?? [], String(name)]);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
for (const id of repeated) atPrompts.push(issue("mt_prompt_id_duplicate", ["prompts"], `More than one prompt has the id "${id}".`));
|
|
4031
|
+
if (once) {
|
|
4032
|
+
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.`));
|
|
4033
|
+
}
|
|
4034
|
+
return [...issues, ...atPrompts];
|
|
4035
|
+
}
|
|
3769
4036
|
// src/schemas/multiple-choice.ts
|
|
3770
4037
|
/**
|
|
3771
4038
|
* Zod schema for a single Multiple Choice option. Loose: unknown keys are
|
|
@@ -4967,6 +5234,35 @@ const RedactedGapSelectDataSchema = zod_v4.strictObject({
|
|
|
4967
5234
|
presentation: zod_v4.literal("dropdown").optional(),
|
|
4968
5235
|
shuffleChoices: zod_v4.boolean().optional()
|
|
4969
5236
|
});
|
|
5237
|
+
/** A redacted matching option: unchanged, because the learner picks from the list. */
|
|
5238
|
+
const RedactedMatchingOptionSchema = zod_v4.strictObject({
|
|
5239
|
+
id: zod_v4.string().min(1),
|
|
5240
|
+
label: zod_v4.string().min(1).optional(),
|
|
5241
|
+
text: zod_v4.string().min(1)
|
|
5242
|
+
});
|
|
5243
|
+
/** A redacted prompt: what the learner reads; `correctOptionId` and feedback are gone. */
|
|
5244
|
+
const RedactedMatchingPromptSchema = zod_v4.strictObject({
|
|
5245
|
+
id: zod_v4.string().min(1),
|
|
5246
|
+
text: zod_v4.string().min(1)
|
|
5247
|
+
});
|
|
5248
|
+
/**
|
|
5249
|
+
* Redacted Matching data. As in Gap Select, the list the learner picks from
|
|
5250
|
+
* survives in full and only the key is withheld: each prompt's
|
|
5251
|
+
* `correctOptionId`. `optionUse` survives because it changes what the learner
|
|
5252
|
+
* may do — an exam says "You may use any letter more than once" in its
|
|
5253
|
+
* instructions, and the component moves an option used once. `scoringStrategy`
|
|
5254
|
+
* stays answer-key, as on every type.
|
|
5255
|
+
*/
|
|
5256
|
+
const RedactedMatchingDataSchema = zod_v4.strictObject({
|
|
5257
|
+
...redactedBase,
|
|
5258
|
+
type: zod_v4.literal("matching"),
|
|
5259
|
+
instructions: zod_v4.string().optional(),
|
|
5260
|
+
optionsTitle: zod_v4.string().optional(),
|
|
5261
|
+
prompts: zod_v4.array(RedactedMatchingPromptSchema).min(1),
|
|
5262
|
+
options: zod_v4.array(RedactedMatchingOptionSchema).min(2),
|
|
5263
|
+
optionUse: zod_v4.enum(["once", "many"]).optional(),
|
|
5264
|
+
shuffleOptions: zod_v4.boolean().optional()
|
|
5265
|
+
});
|
|
4970
5266
|
/**
|
|
4971
5267
|
* Redacted Written Response data: the prompt, word bounds and rubric are
|
|
4972
5268
|
* learner-visible (a rubric tells the learner what they are graded on);
|
|
@@ -5206,6 +5502,48 @@ function scoreGapSelect(data, response) {
|
|
|
5206
5502
|
details
|
|
5207
5503
|
};
|
|
5208
5504
|
}
|
|
5505
|
+
// src/scoring/activity-scorers/matching.ts
|
|
5506
|
+
/**
|
|
5507
|
+
* Scores a Matching response.
|
|
5508
|
+
*
|
|
5509
|
+
* Each prompt is an identity comparison between the option the learner matched
|
|
5510
|
+
* it with and `correctOptionId`, as a gap select's gap is. A prompt with no
|
|
5511
|
+
* match, or matched with an id that is not an option (a tampered payload, or
|
|
5512
|
+
* content edited after the attempt), is scored incorrect and reported as
|
|
5513
|
+
* `incorrect-omission`, so an unanswered prompt stays distinguishable from a
|
|
5514
|
+
* wrong one downstream.
|
|
5515
|
+
*
|
|
5516
|
+
* Under `optionUse: 'once'` a response that uses one option twice is scored
|
|
5517
|
+
* prompt by prompt like any other: the SDK's component never produces one, and
|
|
5518
|
+
* a second use earns nothing the key does not give it, since the key uses each
|
|
5519
|
+
* option at most once.
|
|
5520
|
+
*/
|
|
5521
|
+
function scoreMatching(data, response) {
|
|
5522
|
+
const optionIds = new Set(data.options.map((option) => option.id));
|
|
5523
|
+
const details = [];
|
|
5524
|
+
const perPromptCorrect = [];
|
|
5525
|
+
for (const prompt of data.prompts) {
|
|
5526
|
+
const raw = response.matches[prompt.id];
|
|
5527
|
+
const matched = typeof raw === "string" ? raw : "";
|
|
5528
|
+
const answered = matched !== "" && optionIds.has(matched);
|
|
5529
|
+
const correct = answered && matched === prompt.correctOptionId;
|
|
5530
|
+
perPromptCorrect.push(correct);
|
|
5531
|
+
details.push({
|
|
5532
|
+
itemId: prompt.id,
|
|
5533
|
+
outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
|
|
5534
|
+
learnerResponse: [matched],
|
|
5535
|
+
correctResponse: [prompt.correctOptionId],
|
|
5536
|
+
weight: 1
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
const correctPrompts = perPromptCorrect.filter(Boolean).length;
|
|
5540
|
+
return {
|
|
5541
|
+
score: data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perPromptCorrect) : partialBlankStrategy(correctPrompts, data.prompts.length),
|
|
5542
|
+
maxScore: 1,
|
|
5543
|
+
feedback: null,
|
|
5544
|
+
details
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5209
5547
|
// src/scoring/activity-scorers/multiple-choice.ts
|
|
5210
5548
|
/**
|
|
5211
5549
|
* Scores a Multiple Choice response. Pure; trusts its typed inputs (validation
|
|
@@ -5468,6 +5806,32 @@ const GAP_SELECT_FIELD_POLICY = {
|
|
|
5468
5806
|
feedback: "answer-key"
|
|
5469
5807
|
}
|
|
5470
5808
|
};
|
|
5809
|
+
/**
|
|
5810
|
+
* Matching: as in Gap Select, the list the learner picks from is public, key
|
|
5811
|
+
* by key, and each prompt's `correctOptionId` is the one field withheld.
|
|
5812
|
+
* `optionUse` is public because it changes what the learner may do: the
|
|
5813
|
+
* component moves an option used once, and the paper says so.
|
|
5814
|
+
*/
|
|
5815
|
+
const MATCHING_FIELD_POLICY = {
|
|
5816
|
+
...SHARED_PUBLIC_FIELDS,
|
|
5817
|
+
instructions: "public",
|
|
5818
|
+
optionsTitle: "public",
|
|
5819
|
+
optionUse: "public",
|
|
5820
|
+
shuffleOptions: "public",
|
|
5821
|
+
scoringStrategy: "answer-key",
|
|
5822
|
+
feedback: FEEDBACK_FIELD_POLICY,
|
|
5823
|
+
options: {
|
|
5824
|
+
id: "public",
|
|
5825
|
+
label: "public",
|
|
5826
|
+
text: "public"
|
|
5827
|
+
},
|
|
5828
|
+
prompts: {
|
|
5829
|
+
id: "public",
|
|
5830
|
+
text: "public",
|
|
5831
|
+
correctOptionId: "answer-key",
|
|
5832
|
+
feedback: "answer-key"
|
|
5833
|
+
}
|
|
5834
|
+
};
|
|
5471
5835
|
const FILL_IN_THE_BLANKS_FIELD_POLICY = {
|
|
5472
5836
|
...SHARED_PUBLIC_FIELDS,
|
|
5473
5837
|
instructions: "public",
|
|
@@ -5648,6 +6012,26 @@ const gapSelectType = defineActivityType({
|
|
|
5648
6012
|
authoring: gapSelectAuthoring,
|
|
5649
6013
|
mediaSlots: () => MAIN_MEDIA
|
|
5650
6014
|
});
|
|
6015
|
+
/** Built-in Matching descriptor. */
|
|
6016
|
+
const matchingType = defineActivityType({
|
|
6017
|
+
type: "matching",
|
|
6018
|
+
schema: MatchingDataSchema,
|
|
6019
|
+
scoring: {
|
|
6020
|
+
kind: "sync",
|
|
6021
|
+
score: scoreMatching
|
|
6022
|
+
},
|
|
6023
|
+
isAnswered: (response) => Object.values(response?.matches ?? {}).some((optionId) => optionId !== ""),
|
|
6024
|
+
fieldPolicy: MATCHING_FIELD_POLICY,
|
|
6025
|
+
redactedSchema: RedactedMatchingDataSchema,
|
|
6026
|
+
interop: {
|
|
6027
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
6028
|
+
xapiInteractionType: "matching",
|
|
6029
|
+
correctResponsesPattern: (data) => [data.prompts.map((prompt) => `${prompt.id}[.]${prompt.correctOptionId}`).join("[,]")]
|
|
6030
|
+
},
|
|
6031
|
+
interactions: ["option-matched", "submitted"],
|
|
6032
|
+
authoring: matchingAuthoring,
|
|
6033
|
+
mediaSlots: () => MAIN_MEDIA
|
|
6034
|
+
});
|
|
5651
6035
|
/**
|
|
5652
6036
|
* Built-in Dictation descriptor. Graded synchronously: the score is the
|
|
5653
6037
|
* character-level similarity of the typed text to the transcript, a pure
|
|
@@ -5824,7 +6208,8 @@ const BUILT_IN_DESCRIPTORS = {
|
|
|
5824
6208
|
],
|
|
5825
6209
|
authoring: speakingResponseAuthoring,
|
|
5826
6210
|
mediaSlots: () => MAIN_MEDIA
|
|
5827
|
-
})
|
|
6211
|
+
}),
|
|
6212
|
+
matching: matchingType
|
|
5828
6213
|
};
|
|
5829
6214
|
// src/schemas/read-schema.ts
|
|
5830
6215
|
/**
|
|
@@ -6511,4 +6896,4 @@ Object.defineProperty(exports, "writtenResponseType", {
|
|
|
6511
6896
|
}
|
|
6512
6897
|
});
|
|
6513
6898
|
|
|
6514
|
-
//# sourceMappingURL=registry-
|
|
6899
|
+
//# sourceMappingURL=registry-DUTE1vtq.cjs.map
|