@intellectif/lk-core 0.8.1 → 0.9.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 +50 -0
- package/README.md +8 -6
- package/dist/{chunk-2M76F32Y.js → chunk-6DM2H6BD.js} +2 -2
- package/dist/{chunk-HCOWSGEZ.js → chunk-AHEUYOT3.js} +705 -17
- package/dist/chunk-AHEUYOT3.js.map +1 -0
- package/dist/{chunk-FKH4YT5X.cjs → chunk-CVRALNUK.cjs} +706 -18
- package/dist/chunk-CVRALNUK.cjs.map +1 -0
- package/dist/{chunk-NHGXOOZ2.js → chunk-ELTP5P6V.js} +2 -2
- package/dist/{chunk-YJZY5TPY.cjs → chunk-MOLL5KXO.cjs} +8 -8
- package/dist/{chunk-YJZY5TPY.cjs.map → chunk-MOLL5KXO.cjs.map} +1 -1
- package/dist/{chunk-LNA33IK7.js → chunk-QX7P3CHP.js} +2 -2
- package/dist/{chunk-7ACNBDSF.cjs → chunk-RL2PQLCY.cjs} +4 -4
- package/dist/{chunk-7ACNBDSF.cjs.map → chunk-RL2PQLCY.cjs.map} +1 -1
- package/dist/{chunk-FVLKIL6W.cjs → chunk-YVZCFUGP.cjs} +12 -12
- package/dist/{chunk-FVLKIL6W.cjs.map → chunk-YVZCFUGP.cjs.map} +1 -1
- package/dist/index.cjs +134 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -6
- package/dist/index.d.ts +157 -6
- package/dist/index.js +115 -23
- package/dist/index.js.map +1 -1
- package/dist/schemas.cjs +3 -3
- package/dist/schemas.js +2 -2
- package/dist/scoring.cjs +3 -3
- package/dist/scoring.js +2 -2
- package/dist/xapi.cjs +3 -3
- package/dist/xapi.js +2 -2
- package/package.json +2 -1
- package/vectors/README.md +97 -0
- package/vectors/replay.d.mts +44 -0
- package/vectors/replay.mjs +159 -0
- package/vectors/scoring.json +7698 -0
- package/dist/chunk-FKH4YT5X.cjs.map +0 -1
- package/dist/chunk-HCOWSGEZ.js.map +0 -1
- /package/dist/{chunk-2M76F32Y.js.map → chunk-6DM2H6BD.js.map} +0 -0
- /package/dist/{chunk-NHGXOOZ2.js.map → chunk-ELTP5P6V.js.map} +0 -0
- /package/dist/{chunk-LNA33IK7.js.map → chunk-QX7P3CHP.js.map} +0 -0
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/
|
|
2
|
-
function countWords(text) {
|
|
3
|
-
if (typeof text !== "string") {
|
|
4
|
-
return 0;
|
|
5
|
-
}
|
|
6
|
-
const trimmed = text.trim();
|
|
7
|
-
if (trimmed === "") {
|
|
8
|
-
return 0;
|
|
9
|
-
}
|
|
10
|
-
return trimmed.split(/\s+/).length;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// src/errors.ts
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/errors.ts
|
|
14
2
|
var ActivitySchemaError = class extends Error {
|
|
15
3
|
constructor(activityType, errors) {
|
|
16
4
|
super(`Invalid activity data for type "${activityType}"`);
|
|
@@ -211,6 +199,18 @@ var FillInTheBlanksDataSchema = _v4.z.looseObject({
|
|
|
211
199
|
}
|
|
212
200
|
);
|
|
213
201
|
|
|
202
|
+
// src/count-words.ts
|
|
203
|
+
function countWords(text) {
|
|
204
|
+
if (typeof text !== "string") {
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
const trimmed = text.trim();
|
|
208
|
+
if (trimmed === "") {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
return trimmed.split(/\s+/).length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
214
|
// src/schemas/multiple-choice.ts
|
|
215
215
|
|
|
216
216
|
var MultipleChoiceOptionSchema = _v4.z.looseObject({
|
|
@@ -455,6 +455,687 @@ function registeredActivityTypes() {
|
|
|
455
455
|
return [...registry.keys()];
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// src/authoring/issues.ts
|
|
459
|
+
var DRAFT_ISSUE_SEVERITY = {
|
|
460
|
+
// Every registered type — reported by validateDraft itself
|
|
461
|
+
null_not_allowed: "invalid",
|
|
462
|
+
// Every built-in activity
|
|
463
|
+
schema_version_invalid: "invalid",
|
|
464
|
+
type_mismatch: "invalid",
|
|
465
|
+
id_required: "invalid",
|
|
466
|
+
title_required: "incomplete",
|
|
467
|
+
scoring_strategy_required: "incomplete",
|
|
468
|
+
pass_threshold_invalid: "invalid",
|
|
469
|
+
difficulty_level_invalid: "invalid",
|
|
470
|
+
feedback_empty: "incomplete",
|
|
471
|
+
redacted_data: "invalid",
|
|
472
|
+
media_type_required: "incomplete",
|
|
473
|
+
media_url_required: "incomplete",
|
|
474
|
+
media_url_invalid: "invalid",
|
|
475
|
+
media_alt_required: "incomplete",
|
|
476
|
+
media_playback_invalid: "invalid",
|
|
477
|
+
media_invalid: "invalid",
|
|
478
|
+
// multiple-choice
|
|
479
|
+
mc_question_required: "incomplete",
|
|
480
|
+
mc_mode_required: "incomplete",
|
|
481
|
+
mc_options_too_few: "incomplete",
|
|
482
|
+
mc_options_too_many: "invalid",
|
|
483
|
+
mc_option_id_required: "invalid",
|
|
484
|
+
mc_option_id_duplicate: "invalid",
|
|
485
|
+
mc_option_text_required: "incomplete",
|
|
486
|
+
mc_option_correctness_required: "incomplete",
|
|
487
|
+
mc_correct_option_required: "incomplete",
|
|
488
|
+
mc_single_mode_one_correct: "invalid",
|
|
489
|
+
// fill-in-the-blanks
|
|
490
|
+
fib_passage_required: "incomplete",
|
|
491
|
+
fib_blanks_required: "incomplete",
|
|
492
|
+
fib_blank_id_required: "invalid",
|
|
493
|
+
fib_blank_id_duplicate: "invalid",
|
|
494
|
+
fib_accepted_answers_required: "incomplete",
|
|
495
|
+
fib_accepted_answer_empty: "incomplete",
|
|
496
|
+
fib_levenshtein_invalid: "invalid",
|
|
497
|
+
fib_match_locale_invalid: "invalid",
|
|
498
|
+
fib_match_invalid: "invalid",
|
|
499
|
+
fib_blank_missing: "incomplete",
|
|
500
|
+
fib_placeholder_missing: "incomplete",
|
|
501
|
+
fib_placeholder_duplicate: "invalid",
|
|
502
|
+
fib_blanks_mismatch: "invalid",
|
|
503
|
+
// written-response
|
|
504
|
+
wr_prompt_required: "incomplete",
|
|
505
|
+
wr_min_words_required: "incomplete",
|
|
506
|
+
wr_min_words_invalid: "invalid",
|
|
507
|
+
wr_max_words_required: "incomplete",
|
|
508
|
+
wr_max_words_invalid: "invalid",
|
|
509
|
+
wr_word_bounds_order: "invalid",
|
|
510
|
+
wr_rubric_criteria_required: "incomplete",
|
|
511
|
+
wr_criterion_name_required: "incomplete",
|
|
512
|
+
wr_criterion_weight_required: "incomplete",
|
|
513
|
+
wr_criterion_weight_invalid: "invalid",
|
|
514
|
+
wr_rubric_weights_zero: "incomplete",
|
|
515
|
+
wr_rubric_weights_too_large: "invalid"
|
|
516
|
+
};
|
|
517
|
+
function issue(code, path, message) {
|
|
518
|
+
return { path: path.map(String), message, code, severity: DRAFT_ISSUE_SEVERITY[code] };
|
|
519
|
+
}
|
|
520
|
+
function isRecord(value) {
|
|
521
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
522
|
+
}
|
|
523
|
+
function valueAt(root, path) {
|
|
524
|
+
let node = root;
|
|
525
|
+
for (const segment of path) {
|
|
526
|
+
if (typeof node !== "object" || node === null) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
node = node[segment];
|
|
530
|
+
}
|
|
531
|
+
return node;
|
|
532
|
+
}
|
|
533
|
+
function refusesEmpty(root, schemaIssue) {
|
|
534
|
+
const { path } = schemaIssue;
|
|
535
|
+
const value = valueAt(root, path);
|
|
536
|
+
const index = path.at(-1);
|
|
537
|
+
const list = valueAt(root, path.slice(0, -1));
|
|
538
|
+
const empty = value === null || value === void 0 && typeof index === "number" && Array.isArray(list) && index < list.length;
|
|
539
|
+
return empty && (schemaIssue.input === value || schemaIssue.code === "invalid_union");
|
|
540
|
+
}
|
|
541
|
+
function isUnset(value) {
|
|
542
|
+
return value === void 0 || value === null;
|
|
543
|
+
}
|
|
544
|
+
function isUnwritten(value) {
|
|
545
|
+
return isUnset(value) || typeof value === "string" && value.trim() === "";
|
|
546
|
+
}
|
|
547
|
+
function isMissingId(value) {
|
|
548
|
+
return isUnset(value) || value === "";
|
|
549
|
+
}
|
|
550
|
+
function isWholeNumber(value) {
|
|
551
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
552
|
+
}
|
|
553
|
+
function isTooLarge(value) {
|
|
554
|
+
return typeof value === "number" && value > Number.MAX_SAFE_INTEGER && Number.isInteger(value);
|
|
555
|
+
}
|
|
556
|
+
function checkIdentity(draft, type) {
|
|
557
|
+
const issues = [];
|
|
558
|
+
if (draft.schemaVersion !== "1.0") {
|
|
559
|
+
issues.push(
|
|
560
|
+
issue(
|
|
561
|
+
"schema_version_invalid",
|
|
562
|
+
["schemaVersion"],
|
|
563
|
+
'The draft must have schemaVersion "1.0". A draft from createDraft has it.'
|
|
564
|
+
)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (draft.type !== type) {
|
|
568
|
+
issues.push(issue("type_mismatch", ["type"], `The draft's type must be "${type}".`));
|
|
569
|
+
}
|
|
570
|
+
if (isMissingId(draft.id)) {
|
|
571
|
+
issues.push(issue("id_required", ["id"], "The activity has no id."));
|
|
572
|
+
}
|
|
573
|
+
if (isUnwritten(draft.title)) {
|
|
574
|
+
issues.push(issue("title_required", ["title"], "Add a title."));
|
|
575
|
+
}
|
|
576
|
+
return issues;
|
|
577
|
+
}
|
|
578
|
+
function checkScoringStrategy(draft) {
|
|
579
|
+
return isUnwritten(draft.scoringStrategy) ? [
|
|
580
|
+
issue(
|
|
581
|
+
"scoring_strategy_required",
|
|
582
|
+
["scoringStrategy"],
|
|
583
|
+
"Choose how the question is scored."
|
|
584
|
+
)
|
|
585
|
+
] : [];
|
|
586
|
+
}
|
|
587
|
+
var DIFFICULTY_LEVELS = [1, 2, 3, 4, 5];
|
|
588
|
+
function checkSharedOptional(draft) {
|
|
589
|
+
const issues = [];
|
|
590
|
+
const threshold = draft.passThreshold;
|
|
591
|
+
if (!isUnset(threshold) && !(typeof threshold === "number" && threshold >= 0 && threshold <= 1)) {
|
|
592
|
+
issues.push(
|
|
593
|
+
issue(
|
|
594
|
+
"pass_threshold_invalid",
|
|
595
|
+
["passThreshold"],
|
|
596
|
+
"The pass threshold must be a number from 0 to 1."
|
|
597
|
+
)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (!isUnset(draft.difficultyLevel) && !DIFFICULTY_LEVELS.includes(draft.difficultyLevel)) {
|
|
601
|
+
issues.push(
|
|
602
|
+
issue(
|
|
603
|
+
"difficulty_level_invalid",
|
|
604
|
+
["difficultyLevel"],
|
|
605
|
+
"The difficulty level must be a whole number from 1 to 5."
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const feedback = draft.feedback;
|
|
610
|
+
if (isRecord(feedback)) {
|
|
611
|
+
for (const key of ["correct", "incorrect"]) {
|
|
612
|
+
const message = feedback[key];
|
|
613
|
+
if (typeof message === "string" && message.trim() === "") {
|
|
614
|
+
issues.push(
|
|
615
|
+
issue("feedback_empty", ["feedback", key], "Write this feedback message, or remove it.")
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (draft.redacted === true) {
|
|
621
|
+
issues.push(
|
|
622
|
+
issue(
|
|
623
|
+
"redacted_data",
|
|
624
|
+
["redacted"],
|
|
625
|
+
"This is a copy prepared for learners, with the answer key removed. Edit the original activity instead."
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (isRecord(draft.media)) {
|
|
630
|
+
issues.push(...checkMedia(draft.media));
|
|
631
|
+
}
|
|
632
|
+
return issues;
|
|
633
|
+
}
|
|
634
|
+
var URL_POLICY = 'Use an https:, http:, data: or blob: address, or a path on the same site that starts with a single "/".';
|
|
635
|
+
var EMBED_URL = "An embed needs the provider's full http(s) embed address, such as https://www.youtube.com/embed/VIDEO_ID.";
|
|
636
|
+
function checkMedia(media) {
|
|
637
|
+
const issues = [];
|
|
638
|
+
const parsed = MediaSchema.safeParse(media, { reportInput: true });
|
|
639
|
+
const schemaIssues = parsed.success ? [] : parsed.error.issues.filter((found) => !refusesEmpty(media, found));
|
|
640
|
+
const refusal = (field) => schemaIssues.find((found) => found.path[0] === field);
|
|
641
|
+
const typeUnset = isUnwritten(media.type);
|
|
642
|
+
if (typeUnset) {
|
|
643
|
+
issues.push(
|
|
644
|
+
issue("media_type_required", ["media", "type"], "Choose what kind of media this is.")
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (isUnwritten(media.url)) {
|
|
648
|
+
issues.push(
|
|
649
|
+
issue("media_url_required", ["media", "url"], "Add the address of the media file.")
|
|
650
|
+
);
|
|
651
|
+
} else {
|
|
652
|
+
const refused = refusal("url");
|
|
653
|
+
if (refused !== void 0) {
|
|
654
|
+
issues.push(
|
|
655
|
+
issue(
|
|
656
|
+
"media_url_invalid",
|
|
657
|
+
["media", "url"],
|
|
658
|
+
refused.code === "custom" ? EMBED_URL : URL_POLICY
|
|
659
|
+
)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const needsAlt = media.type === "image" || media.type === "embed";
|
|
664
|
+
const alt = media.alt;
|
|
665
|
+
if (needsAlt && isUnwritten(alt) || typeof alt === "string" && alt.trim() === "") {
|
|
666
|
+
issues.push(
|
|
667
|
+
issue("media_alt_required", ["media", "alt"], "Add a text description of the media.")
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const captions = media.captionsUrl;
|
|
671
|
+
if (typeof captions === "string" && captions.trim() === "") {
|
|
672
|
+
issues.push(
|
|
673
|
+
issue(
|
|
674
|
+
"media_url_required",
|
|
675
|
+
["media", "captionsUrl"],
|
|
676
|
+
"Add the address of the captions file, or remove the captions."
|
|
677
|
+
)
|
|
678
|
+
);
|
|
679
|
+
} else if (refusal("captionsUrl") !== void 0) {
|
|
680
|
+
issues.push(issue("media_url_invalid", ["media", "captionsUrl"], URL_POLICY));
|
|
681
|
+
}
|
|
682
|
+
for (const schemaIssue of schemaIssues) {
|
|
683
|
+
const field = schemaIssue.path[0];
|
|
684
|
+
if (field === "url" || field === "alt" || field === "captionsUrl" || field === "type" && typeUnset) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
issues.push(
|
|
688
|
+
issue(
|
|
689
|
+
field === "playback" ? "media_playback_invalid" : "media_invalid",
|
|
690
|
+
["media", ...schemaIssue.path.map(String)],
|
|
691
|
+
schemaIssue.message
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return issues;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/authoring/fill-in-the-blanks.ts
|
|
699
|
+
var fillInTheBlanksAuthoring = {
|
|
700
|
+
createDraft: ({ newId }) => ({
|
|
701
|
+
schemaVersion: "1.0",
|
|
702
|
+
type: "fill-in-the-blanks",
|
|
703
|
+
id: newId(),
|
|
704
|
+
title: "",
|
|
705
|
+
passage: "",
|
|
706
|
+
blanks: [],
|
|
707
|
+
scoringStrategy: "all-or-nothing"
|
|
708
|
+
}),
|
|
709
|
+
checkDraft: checkFillInTheBlanksDraft
|
|
710
|
+
};
|
|
711
|
+
function checkFillInTheBlanksDraft(draft) {
|
|
712
|
+
const issues = checkIdentity(draft, "fill-in-the-blanks");
|
|
713
|
+
const passage = draft.passage;
|
|
714
|
+
if (isUnwritten(passage)) {
|
|
715
|
+
issues.push(issue("fib_passage_required", ["passage"], "Write the passage."));
|
|
716
|
+
}
|
|
717
|
+
issues.push(...checkScoringStrategy(draft));
|
|
718
|
+
const blanks = isUnset(draft.blanks) ? [] : draft.blanks;
|
|
719
|
+
if (Array.isArray(blanks)) {
|
|
720
|
+
if (blanks.length === 0) {
|
|
721
|
+
issues.push(
|
|
722
|
+
issue(
|
|
723
|
+
"fib_blanks_required",
|
|
724
|
+
["blanks"],
|
|
725
|
+
"Add at least one blank, and mark where it goes in the passage with {{id}}."
|
|
726
|
+
)
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const ids = [];
|
|
730
|
+
for (const [index, blank] of blanks.entries()) {
|
|
731
|
+
if (!isRecord(blank)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const name = typeof blank.id === "string" && blank.id !== "" ? `"${blank.id}"` : index + 1;
|
|
735
|
+
if (isMissingId(blank.id)) {
|
|
736
|
+
issues.push(
|
|
737
|
+
issue("fib_blank_id_required", ["blanks", index, "id"], `Blank ${index + 1} has no id.`)
|
|
738
|
+
);
|
|
739
|
+
} else if (typeof blank.id === "string") {
|
|
740
|
+
ids.push(blank.id);
|
|
741
|
+
}
|
|
742
|
+
const answers = isUnset(blank.acceptedAnswers) ? [] : blank.acceptedAnswers;
|
|
743
|
+
if (Array.isArray(answers)) {
|
|
744
|
+
if (answers.length === 0) {
|
|
745
|
+
issues.push(
|
|
746
|
+
issue(
|
|
747
|
+
"fib_accepted_answers_required",
|
|
748
|
+
["blanks", index, "acceptedAnswers"],
|
|
749
|
+
`Add an accepted answer for blank ${name}.`
|
|
750
|
+
)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
for (const [position, answer] of answers.entries()) {
|
|
754
|
+
if (typeof answer === "string" && answer.trim() === "") {
|
|
755
|
+
issues.push(
|
|
756
|
+
issue(
|
|
757
|
+
"fib_accepted_answer_empty",
|
|
758
|
+
["blanks", index, "acceptedAnswers", position],
|
|
759
|
+
`An accepted answer for blank ${name} is empty. Fill it in or remove it.`
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (isRecord(blank.match)) {
|
|
766
|
+
issues.push(...checkMatch(blank.match, index));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (typeof passage === "string" && passage.trim() !== "") {
|
|
770
|
+
issues.push(...checkPairing(passage, blanks, ids));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
issues.push(...checkSharedOptional(draft));
|
|
774
|
+
return issues;
|
|
775
|
+
}
|
|
776
|
+
function checkMatch(match, index) {
|
|
777
|
+
const issues = [];
|
|
778
|
+
const at = (...rest) => ["blanks", index, "match", ...rest];
|
|
779
|
+
const tolerance = match.levenshtein;
|
|
780
|
+
if (!isUnset(tolerance) && !(isWholeNumber(tolerance) && tolerance >= 0)) {
|
|
781
|
+
issues.push(
|
|
782
|
+
issue(
|
|
783
|
+
"fib_levenshtein_invalid",
|
|
784
|
+
at("levenshtein"),
|
|
785
|
+
isTooLarge(tolerance) ? "Typo tolerance is too large." : "Typo tolerance must be a whole number, 0 or more."
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const locale = match.locale;
|
|
790
|
+
if (typeof locale === "string" && locale !== "" && !isLanguageTag(locale)) {
|
|
791
|
+
issues.push(
|
|
792
|
+
issue(
|
|
793
|
+
"fib_match_locale_invalid",
|
|
794
|
+
at("locale"),
|
|
795
|
+
`"${locale}" is not a language tag. Use one such as "tr" or "en-US", or leave the locale out.`
|
|
796
|
+
)
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const parsed = TextMatchPolicySchema.safeParse(match, { reportInput: true });
|
|
800
|
+
if (!parsed.success) {
|
|
801
|
+
for (const schemaIssue of parsed.error.issues) {
|
|
802
|
+
if (schemaIssue.path[0] === "levenshtein" || refusesEmpty(match, schemaIssue)) {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
issues.push(
|
|
806
|
+
issue("fib_match_invalid", at(...schemaIssue.path.map(String)), schemaIssue.message)
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return issues;
|
|
811
|
+
}
|
|
812
|
+
function isLanguageTag(tag) {
|
|
813
|
+
try {
|
|
814
|
+
Intl.getCanonicalLocales(tag);
|
|
815
|
+
return true;
|
|
816
|
+
} catch (e) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function checkPairing(passage, blanks, ids) {
|
|
821
|
+
const issues = [];
|
|
822
|
+
const placeholders = /* @__PURE__ */ new Map();
|
|
823
|
+
for (const match of passage.matchAll(PLACEHOLDER_RE)) {
|
|
824
|
+
const id = match[1];
|
|
825
|
+
placeholders.set(id, (_nullishCoalesce(placeholders.get(id), () => ( 0))) + 1);
|
|
826
|
+
}
|
|
827
|
+
const blankIds = /* @__PURE__ */ new Map();
|
|
828
|
+
for (const id of ids) {
|
|
829
|
+
blankIds.set(id, (_nullishCoalesce(blankIds.get(id), () => ( 0))) + 1);
|
|
830
|
+
}
|
|
831
|
+
for (const [id, count] of blankIds) {
|
|
832
|
+
if (count > 1) {
|
|
833
|
+
issues.push(
|
|
834
|
+
issue("fib_blank_id_duplicate", ["passage"], `More than one blank has the id "${id}".`)
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const [id, count] of placeholders) {
|
|
839
|
+
if (count > 1) {
|
|
840
|
+
issues.push(
|
|
841
|
+
issue(
|
|
842
|
+
"fib_placeholder_duplicate",
|
|
843
|
+
["passage"],
|
|
844
|
+
`{{${id}}} appears more than once in the passage. Each blank goes in one place.`
|
|
845
|
+
)
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (!blankIds.has(id)) {
|
|
849
|
+
issues.push(
|
|
850
|
+
issue(
|
|
851
|
+
"fib_blank_missing",
|
|
852
|
+
["passage"],
|
|
853
|
+
`The passage has {{${id}}}, but there is no blank with that id.`
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const id of blankIds.keys()) {
|
|
859
|
+
if (!placeholders.has(id)) {
|
|
860
|
+
issues.push(
|
|
861
|
+
issue(
|
|
862
|
+
"fib_placeholder_missing",
|
|
863
|
+
["passage"],
|
|
864
|
+
`Blank "${id}" is not in the passage. Put {{${id}}} where it goes.`
|
|
865
|
+
)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (issues.length === 0 && !pairsOneToOne(placeholders, blanks)) {
|
|
870
|
+
issues.push(
|
|
871
|
+
issue(
|
|
872
|
+
"fib_blanks_mismatch",
|
|
873
|
+
["passage"],
|
|
874
|
+
"The blanks and the {{id}} placeholders in the passage do not pair up one to one."
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return issues;
|
|
879
|
+
}
|
|
880
|
+
function pairsOneToOne(placeholders, blanks) {
|
|
881
|
+
const blankIds = blanks.filter(isRecord).map((blank) => blank.id);
|
|
882
|
+
if (new Set(blankIds).size !== blankIds.length) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
if (placeholders.size !== blankIds.length) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
return blankIds.every((id) => typeof id === "string" && placeholders.get(id) === 1);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/authoring/multiple-choice.ts
|
|
892
|
+
var MIN_OPTIONS = 2;
|
|
893
|
+
var MAX_OPTIONS = 26;
|
|
894
|
+
var multipleChoiceAuthoring = {
|
|
895
|
+
createDraft: ({ newId }) => ({
|
|
896
|
+
schemaVersion: "1.0",
|
|
897
|
+
type: "multiple-choice",
|
|
898
|
+
id: newId(),
|
|
899
|
+
title: "",
|
|
900
|
+
question: "",
|
|
901
|
+
mode: "single",
|
|
902
|
+
scoringStrategy: "all-or-nothing",
|
|
903
|
+
options: [
|
|
904
|
+
{ id: newId(), text: "", isCorrect: false },
|
|
905
|
+
{ id: newId(), text: "", isCorrect: false }
|
|
906
|
+
]
|
|
907
|
+
}),
|
|
908
|
+
checkDraft: checkMultipleChoiceDraft
|
|
909
|
+
};
|
|
910
|
+
function checkMultipleChoiceDraft(draft) {
|
|
911
|
+
const issues = checkIdentity(draft, "multiple-choice");
|
|
912
|
+
if (isUnwritten(draft.question)) {
|
|
913
|
+
issues.push(issue("mc_question_required", ["question"], "Write the question."));
|
|
914
|
+
}
|
|
915
|
+
if (isUnwritten(draft.mode)) {
|
|
916
|
+
issues.push(
|
|
917
|
+
issue("mc_mode_required", ["mode"], "Choose whether learners select one option or several.")
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
issues.push(...checkScoringStrategy(draft));
|
|
921
|
+
const options = isUnset(draft.options) ? [] : draft.options;
|
|
922
|
+
if (Array.isArray(options)) {
|
|
923
|
+
if (options.length < MIN_OPTIONS) {
|
|
924
|
+
issues.push(issue("mc_options_too_few", ["options"], "Add at least two options."));
|
|
925
|
+
}
|
|
926
|
+
if (options.length > MAX_OPTIONS) {
|
|
927
|
+
issues.push(
|
|
928
|
+
issue("mc_options_too_many", ["options"], `Use no more than ${MAX_OPTIONS} options.`)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
932
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
933
|
+
let correct = 0;
|
|
934
|
+
for (const [index, option] of options.entries()) {
|
|
935
|
+
if (!isRecord(option)) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
const ordinal = index + 1;
|
|
939
|
+
if (isMissingId(option.id)) {
|
|
940
|
+
issues.push(
|
|
941
|
+
issue("mc_option_id_required", ["options", index, "id"], `Option ${ordinal} has no id.`)
|
|
942
|
+
);
|
|
943
|
+
} else if (typeof option.id === "string") {
|
|
944
|
+
if (seen.has(option.id)) {
|
|
945
|
+
repeated.add(option.id);
|
|
946
|
+
}
|
|
947
|
+
seen.add(option.id);
|
|
948
|
+
}
|
|
949
|
+
if (isUnwritten(option.text)) {
|
|
950
|
+
issues.push(
|
|
951
|
+
issue(
|
|
952
|
+
"mc_option_text_required",
|
|
953
|
+
["options", index, "text"],
|
|
954
|
+
`Write the text of option ${ordinal}.`
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
if (isUnset(option.isCorrect)) {
|
|
959
|
+
issues.push(
|
|
960
|
+
issue(
|
|
961
|
+
"mc_option_correctness_required",
|
|
962
|
+
["options", index, "isCorrect"],
|
|
963
|
+
`Say whether option ${ordinal} is correct.`
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (option.isCorrect === true) {
|
|
968
|
+
correct += 1;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (const id of repeated) {
|
|
972
|
+
issues.push(
|
|
973
|
+
issue("mc_option_id_duplicate", ["options"], `More than one option has the id "${id}".`)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (correct === 0) {
|
|
977
|
+
issues.push(
|
|
978
|
+
issue(
|
|
979
|
+
"mc_correct_option_required",
|
|
980
|
+
["options"],
|
|
981
|
+
draft.mode === "multi" ? "Mark at least one option as correct." : "Mark the correct option."
|
|
982
|
+
)
|
|
983
|
+
);
|
|
984
|
+
} else if (draft.mode === "single" && correct > 1) {
|
|
985
|
+
issues.push(
|
|
986
|
+
issue(
|
|
987
|
+
"mc_single_mode_one_correct",
|
|
988
|
+
["options"],
|
|
989
|
+
"Only one option can be marked correct when learners select one option."
|
|
990
|
+
)
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
issues.push(...checkSharedOptional(draft));
|
|
995
|
+
return issues;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/authoring/written-response.ts
|
|
999
|
+
var writtenResponseAuthoring = {
|
|
1000
|
+
createDraft: ({ newId }) => ({
|
|
1001
|
+
schemaVersion: "1.0",
|
|
1002
|
+
type: "written-response",
|
|
1003
|
+
id: newId(),
|
|
1004
|
+
title: "",
|
|
1005
|
+
prompt: "",
|
|
1006
|
+
minWords: 0,
|
|
1007
|
+
maxWords: 0
|
|
1008
|
+
}),
|
|
1009
|
+
checkDraft: checkWrittenResponseDraft
|
|
1010
|
+
};
|
|
1011
|
+
function checkWrittenResponseDraft(draft) {
|
|
1012
|
+
const issues = checkIdentity(draft, "written-response");
|
|
1013
|
+
if (isUnwritten(draft.prompt)) {
|
|
1014
|
+
issues.push(
|
|
1015
|
+
issue(
|
|
1016
|
+
"wr_prompt_required",
|
|
1017
|
+
["prompt"],
|
|
1018
|
+
isUnwritten(draft.promptHtml) ? "Write the prompt." : "Write the prompt as plain text too. The rich-text prompt is shown only where a sanitiser is supplied; the plain text is shown everywhere else."
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const min = draft.minWords;
|
|
1023
|
+
const max = draft.maxWords;
|
|
1024
|
+
if (isUnset(min)) {
|
|
1025
|
+
issues.push(
|
|
1026
|
+
issue(
|
|
1027
|
+
"wr_min_words_required",
|
|
1028
|
+
["minWords"],
|
|
1029
|
+
"Set the minimum number of words. Use 0 for no minimum."
|
|
1030
|
+
)
|
|
1031
|
+
);
|
|
1032
|
+
} else if (!(isWholeNumber(min) && min >= 0)) {
|
|
1033
|
+
issues.push(
|
|
1034
|
+
issue(
|
|
1035
|
+
"wr_min_words_invalid",
|
|
1036
|
+
["minWords"],
|
|
1037
|
+
isTooLarge(min) ? "The minimum word count is too large." : "The minimum word count must be a whole number, 0 or more."
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
if (isUnset(max) || max === 0) {
|
|
1042
|
+
issues.push(issue("wr_max_words_required", ["maxWords"], "Set the maximum number of words."));
|
|
1043
|
+
} else if (!(isWholeNumber(max) && max >= 1)) {
|
|
1044
|
+
issues.push(
|
|
1045
|
+
issue(
|
|
1046
|
+
"wr_max_words_invalid",
|
|
1047
|
+
["maxWords"],
|
|
1048
|
+
isTooLarge(max) ? "The maximum word count is too large." : "The maximum word count must be a whole number, 1 or more."
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
} else if (typeof min === "number" && max < min) {
|
|
1052
|
+
issues.push(
|
|
1053
|
+
issue(
|
|
1054
|
+
"wr_word_bounds_order",
|
|
1055
|
+
["maxWords"],
|
|
1056
|
+
"The maximum word count cannot be lower than the minimum."
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
const rubric = draft.rubric;
|
|
1061
|
+
if (isRecord(rubric)) {
|
|
1062
|
+
const criteria = isUnset(rubric.criteria) ? [] : rubric.criteria;
|
|
1063
|
+
if (Array.isArray(criteria)) {
|
|
1064
|
+
issues.push(...checkCriteria(criteria));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
issues.push(...checkSharedOptional(draft));
|
|
1068
|
+
return issues;
|
|
1069
|
+
}
|
|
1070
|
+
function checkCriteria(criteria) {
|
|
1071
|
+
const issues = [];
|
|
1072
|
+
if (criteria.length === 0) {
|
|
1073
|
+
issues.push(
|
|
1074
|
+
issue(
|
|
1075
|
+
"wr_rubric_criteria_required",
|
|
1076
|
+
["rubric", "criteria"],
|
|
1077
|
+
"Add at least one rubric criterion, or remove the rubric."
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
return issues;
|
|
1081
|
+
}
|
|
1082
|
+
let totalWeight = 0;
|
|
1083
|
+
let everyWeightUsable = true;
|
|
1084
|
+
for (const [index, criterion] of criteria.entries()) {
|
|
1085
|
+
if (!isRecord(criterion)) {
|
|
1086
|
+
everyWeightUsable = false;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const ordinal = index + 1;
|
|
1090
|
+
const path = ["rubric", "criteria", index];
|
|
1091
|
+
if (isUnwritten(criterion.name)) {
|
|
1092
|
+
issues.push(
|
|
1093
|
+
issue("wr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`)
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
const weight = criterion.weight;
|
|
1097
|
+
if (isUnset(weight)) {
|
|
1098
|
+
everyWeightUsable = false;
|
|
1099
|
+
issues.push(
|
|
1100
|
+
issue(
|
|
1101
|
+
"wr_criterion_weight_required",
|
|
1102
|
+
[...path, "weight"],
|
|
1103
|
+
`Give rubric criterion ${ordinal} a weight.`
|
|
1104
|
+
)
|
|
1105
|
+
);
|
|
1106
|
+
} else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
|
|
1107
|
+
everyWeightUsable = false;
|
|
1108
|
+
issues.push(
|
|
1109
|
+
issue(
|
|
1110
|
+
"wr_criterion_weight_invalid",
|
|
1111
|
+
[...path, "weight"],
|
|
1112
|
+
"A rubric weight must be a number, 0 or more."
|
|
1113
|
+
)
|
|
1114
|
+
);
|
|
1115
|
+
} else {
|
|
1116
|
+
totalWeight += weight;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (everyWeightUsable && totalWeight === 0) {
|
|
1120
|
+
issues.push(
|
|
1121
|
+
issue(
|
|
1122
|
+
"wr_rubric_weights_zero",
|
|
1123
|
+
["rubric", "criteria"],
|
|
1124
|
+
"Give at least one rubric criterion a weight above 0. With every weight at 0, no weighted total can be computed."
|
|
1125
|
+
)
|
|
1126
|
+
);
|
|
1127
|
+
} else if (totalWeight === Number.POSITIVE_INFINITY) {
|
|
1128
|
+
issues.push(
|
|
1129
|
+
issue(
|
|
1130
|
+
"wr_rubric_weights_too_large",
|
|
1131
|
+
["rubric", "criteria"],
|
|
1132
|
+
"The rubric weights add up to more than can be calculated with. Use smaller weights."
|
|
1133
|
+
)
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
return issues;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
458
1139
|
// src/scoring/strategies/all-or-nothing.ts
|
|
459
1140
|
function allOrNothingStrategy(correctItems) {
|
|
460
1141
|
return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
|
|
@@ -660,7 +1341,8 @@ var multipleChoiceType = defineActivityType({
|
|
|
660
1341
|
data.options.filter((option) => option.isCorrect).map((option) => option.id).join("[,]")
|
|
661
1342
|
]
|
|
662
1343
|
},
|
|
663
|
-
interactions: ["option-selected", "option-deselected", "submitted"]
|
|
1344
|
+
interactions: ["option-selected", "option-deselected", "submitted"],
|
|
1345
|
+
authoring: multipleChoiceAuthoring
|
|
664
1346
|
});
|
|
665
1347
|
var fillInTheBlanksType = defineActivityType({
|
|
666
1348
|
type: "fill-in-the-blanks",
|
|
@@ -679,7 +1361,8 @@ var fillInTheBlanksType = defineActivityType({
|
|
|
679
1361
|
data.blanks.map((blank) => _nullishCoalesce(blank.acceptedAnswers[0], () => ( ""))).join("[,]")
|
|
680
1362
|
]
|
|
681
1363
|
},
|
|
682
|
-
interactions: ["blank-filled", "hint-requested", "submitted"]
|
|
1364
|
+
interactions: ["blank-filled", "hint-requested", "submitted"],
|
|
1365
|
+
authoring: fillInTheBlanksAuthoring
|
|
683
1366
|
});
|
|
684
1367
|
var writtenResponseType = defineActivityType({
|
|
685
1368
|
type: "written-response",
|
|
@@ -703,7 +1386,8 @@ var writtenResponseType = defineActivityType({
|
|
|
703
1386
|
xapiInteractionType: "long-fill-in",
|
|
704
1387
|
correctResponsesPattern: () => []
|
|
705
1388
|
},
|
|
706
|
-
interactions: ["text-changed", "submitted"]
|
|
1389
|
+
interactions: ["text-changed", "submitted"],
|
|
1390
|
+
authoring: writtenResponseAuthoring
|
|
707
1391
|
});
|
|
708
1392
|
registerActivityType(multipleChoiceType);
|
|
709
1393
|
registerActivityType(fillInTheBlanksType);
|
|
@@ -744,5 +1428,9 @@ registerActivityType(writtenResponseType);
|
|
|
744
1428
|
|
|
745
1429
|
|
|
746
1430
|
|
|
747
|
-
|
|
748
|
-
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
exports.ActivitySchemaError = ActivitySchemaError; exports.UnknownActivityTypeError = UnknownActivityTypeError; exports.RedactedScoringError = RedactedScoringError; exports.DeferredScoringError = DeferredScoringError; exports.FeedbackSchema = FeedbackSchema; exports.MediaUrlSchema = MediaUrlSchema; exports.NativeControlHintSchema = NativeControlHintSchema; exports.MediaPlaybackSchema = MediaPlaybackSchema; exports.MediaSchema = MediaSchema; exports.RedactedMediaSchema = RedactedMediaSchema; exports.TextMatchPolicySchema = TextMatchPolicySchema; exports.BlankConfigSchema = BlankConfigSchema; exports.FillInTheBlanksDataSchema = FillInTheBlanksDataSchema; exports.DRAFT_ISSUE_SEVERITY = DRAFT_ISSUE_SEVERITY; exports.issue = issue; exports.isRecord = isRecord; exports.refusesEmpty = refusesEmpty; exports.countWords = countWords; exports.MultipleChoiceOptionSchema = MultipleChoiceOptionSchema; exports.MultipleChoiceDataSchema = MultipleChoiceDataSchema; exports.WrittenResponseRubricCriterionSchema = WrittenResponseRubricCriterionSchema; exports.WrittenResponseRubricSchema = WrittenResponseRubricSchema; exports.WrittenResponseDataSchema = WrittenResponseDataSchema; exports.RedactedMultipleChoiceOptionSchema = RedactedMultipleChoiceOptionSchema; exports.RedactedMultipleChoiceDataSchema = RedactedMultipleChoiceDataSchema; exports.RedactedBlankConfigSchema = RedactedBlankConfigSchema; exports.RedactedFillInTheBlanksDataSchema = RedactedFillInTheBlanksDataSchema; exports.RedactedWrittenResponseDataSchema = RedactedWrittenResponseDataSchema; exports.levenshteinDistance = levenshteinDistance; exports.matchText = matchText; exports.defineActivityType = defineActivityType; exports.registerActivityType = registerActivityType; exports.getActivityTypeDescriptor = getActivityTypeDescriptor; exports.registeredActivityTypes = registeredActivityTypes; exports.MEDIA_FIELD_POLICY = MEDIA_FIELD_POLICY; exports.multipleChoiceType = multipleChoiceType; exports.fillInTheBlanksType = fillInTheBlanksType; exports.writtenResponseType = writtenResponseType;
|
|
1436
|
+
//# sourceMappingURL=chunk-CVRALNUK.cjs.map
|