@intellectif/lk-core 1.7.0 → 1.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 +33 -0
- package/dist/ai-check.cjs +77 -3
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +11 -3
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +11 -3
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +77 -3
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-B7oMCt66.js → ai-writing-DQjI9EFN.js} +2514 -136
- package/dist/ai-writing-DQjI9EFN.js.map +1 -0
- package/dist/{ai-writing-DrPTV3R2.cjs → ai-writing-DbpZpGRk.cjs} +2614 -122
- package/dist/ai-writing-DbpZpGRk.cjs.map +1 -0
- package/dist/{ai-B1wYrqcB.d.ts → ai-zhc9yhRj.d.cts} +454 -2
- package/dist/ai-zhc9yhRj.d.cts.map +1 -0
- package/dist/{ai-B1wYrqcB.d.cts → ai-zhc9yhRj.d.ts} +454 -2
- package/dist/ai-zhc9yhRj.d.ts.map +1 -0
- package/dist/{index-XCCoPoTU.d.cts → index-BeC8tAWf.d.cts} +2 -2
- package/dist/{index-XCCoPoTU.d.cts.map → index-BeC8tAWf.d.cts.map} +1 -1
- package/dist/{index-BU99GFTl.d.ts → index-Byar5VVs.d.ts} +2 -2
- package/dist/{index-BU99GFTl.d.ts.map → index-Byar5VVs.d.ts.map} +1 -1
- package/dist/{index-DP9rvyOz.d.cts → index-Ccj8oL_E.d.cts} +2 -2
- package/dist/{index-DP9rvyOz.d.cts.map → index-Ccj8oL_E.d.cts.map} +1 -1
- package/dist/{index-BTkMQtws.d.ts → index-CxkqS62X.d.ts} +89 -7
- package/dist/index-CxkqS62X.d.ts.map +1 -0
- package/dist/{index-BM8LeDWn.d.cts → index-DLsUnGHt.d.cts} +89 -7
- package/dist/index-DLsUnGHt.d.cts.map +1 -0
- package/dist/{index-BdXs4-en.d.ts → index-f4YuMgFb.d.ts} +2 -2
- package/dist/{index-BdXs4-en.d.ts.map → index-f4YuMgFb.d.ts.map} +1 -1
- package/dist/index.cjs +282 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +262 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +262 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +269 -7
- package/dist/index.js.map +1 -1
- package/dist/{item-group-CeDk7mfM.js → item-group-CKZk3txt.js} +2 -2
- package/dist/{item-group-CeDk7mfM.js.map → item-group-CKZk3txt.js.map} +1 -1
- package/dist/{item-group-8P3WFuUK.cjs → item-group-COP5OwEC.cjs} +2 -2
- package/dist/{item-group-8P3WFuUK.cjs.map → item-group-COP5OwEC.cjs.map} +1 -1
- package/dist/{registry-D92hz9iA.js → registry-Bw9grsCN.js} +339 -12
- package/dist/registry-Bw9grsCN.js.map +1 -0
- package/dist/{registry-hzrkTeIT.cjs → registry-C9O9txy5.cjs} +368 -11
- package/dist/registry-C9O9txy5.cjs.map +1 -0
- package/dist/{schemas-C7TViWCg.js → schemas-BJe3W6Xb.js} +3 -3
- package/dist/{schemas-C7TViWCg.js.map → schemas-BJe3W6Xb.js.map} +1 -1
- package/dist/{schemas-BWeskONy.cjs → schemas-CXlemRvB.cjs} +3 -3
- package/dist/{schemas-BWeskONy.cjs.map → schemas-CXlemRvB.cjs.map} +1 -1
- package/dist/schemas.cjs +3 -3
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +3 -3
- package/dist/{scoring-DD1GHcLI.js → scoring-BGKdff2i.js} +214 -26
- package/dist/scoring-BGKdff2i.js.map +1 -0
- package/dist/{scoring-CtkCFyFT.cjs → scoring-Db1CZeAn.cjs} +219 -25
- package/dist/scoring-Db1CZeAn.cjs.map +1 -0
- package/dist/scoring.cjs +3 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-Cgicg-zU.js → xapi-BtBm8AWZ.js} +2 -2
- package/dist/{xapi-Cgicg-zU.js.map → xapi-BtBm8AWZ.js.map} +1 -1
- package/dist/{xapi-7btSpx6v.cjs → xapi-DOAqH9__.cjs} +2 -2
- package/dist/{xapi-7btSpx6v.cjs.map → xapi-DOAqH9__.cjs.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 +2 -2
- package/vectors/README.md +23 -0
- package/vectors/scoring.json +11967 -1
- package/vectors/validation.json +1298 -1
- package/vectors/variants.d.mts +20 -0
- package/vectors/variants.json +5753 -0
- package/vectors/variants.mjs +67 -0
- package/dist/ai-B1wYrqcB.d.cts.map +0 -1
- package/dist/ai-B1wYrqcB.d.ts.map +0 -1
- package/dist/ai-writing-B7oMCt66.js.map +0 -1
- package/dist/ai-writing-DrPTV3R2.cjs.map +0 -1
- package/dist/index-BM8LeDWn.d.cts.map +0 -1
- package/dist/index-BTkMQtws.d.ts.map +0 -1
- package/dist/registry-D92hz9iA.js.map +0 -1
- package/dist/registry-hzrkTeIT.cjs.map +0 -1
- package/dist/scoring-CtkCFyFT.cjs.map +0 -1
- package/dist/scoring-DD1GHcLI.js.map +0 -1
|
@@ -441,7 +441,8 @@ const BUILT_IN_ACTIVITY_TYPES = [
|
|
|
441
441
|
"written-response",
|
|
442
442
|
"gap-select",
|
|
443
443
|
"dictation",
|
|
444
|
-
"read-aloud"
|
|
444
|
+
"read-aloud",
|
|
445
|
+
"speaking-response"
|
|
445
446
|
];
|
|
446
447
|
/** Whether `type` names an activity type the SDK ships. */
|
|
447
448
|
function isBuiltInActivityType(type) {
|
|
@@ -2349,7 +2350,24 @@ const DRAFT_ISSUE_SEVERITY = {
|
|
|
2349
2350
|
ra_media_kind: "invalid",
|
|
2350
2351
|
ra_slow_media_without_media: "incomplete",
|
|
2351
2352
|
ra_slow_media_same_recording: "invalid",
|
|
2352
|
-
ra_slow_media_beside_play_limit: "invalid"
|
|
2353
|
+
ra_slow_media_beside_play_limit: "invalid",
|
|
2354
|
+
sr_prompt_required: "incomplete",
|
|
2355
|
+
sr_locale_required: "incomplete",
|
|
2356
|
+
sr_locale_invalid: "invalid",
|
|
2357
|
+
sr_max_seconds_required: "incomplete",
|
|
2358
|
+
sr_max_seconds_out_of_range: "invalid",
|
|
2359
|
+
sr_min_seconds_out_of_range: "invalid",
|
|
2360
|
+
sr_max_takes_out_of_range: "invalid",
|
|
2361
|
+
sr_rubric_required: "incomplete",
|
|
2362
|
+
sr_rubric_criteria_required: "incomplete",
|
|
2363
|
+
sr_criterion_name_required: "incomplete",
|
|
2364
|
+
sr_criterion_name_duplicate: "invalid",
|
|
2365
|
+
sr_criterion_weight_required: "incomplete",
|
|
2366
|
+
sr_criterion_weight_invalid: "invalid",
|
|
2367
|
+
sr_criterion_measure_invalid: "invalid",
|
|
2368
|
+
sr_criterion_measure_duplicate: "invalid",
|
|
2369
|
+
sr_rubric_weights_zero: "incomplete",
|
|
2370
|
+
sr_rubric_weights_too_large: "invalid"
|
|
2353
2371
|
};
|
|
2354
2372
|
function issue(code, path, message) {
|
|
2355
2373
|
return {
|
|
@@ -2564,7 +2582,7 @@ function checkDictationDraft(draft) {
|
|
|
2564
2582
|
const normalizer = dictationNormalizer(isRecord(draft.tolerance) ? draft.tolerance : void 0);
|
|
2565
2583
|
issues.push(...checkTranscript(draft.transcript, normalizer));
|
|
2566
2584
|
issues.push(...checkAcceptedTranscripts(draft, normalizer));
|
|
2567
|
-
issues.push(...checkRecording$
|
|
2585
|
+
issues.push(...checkRecording$2(draft.media));
|
|
2568
2586
|
issues.push(...checkSlowRecording$1(draft));
|
|
2569
2587
|
issues.push(...checkRevealed(draft, normalizer));
|
|
2570
2588
|
issues.push(...checkHints(draft.hints));
|
|
@@ -2622,7 +2640,7 @@ function acceptedTranscriptEmpty(index) {
|
|
|
2622
2640
|
return issue("dc_accepted_transcript_empty", ["acceptedTranscripts", index], `Accepted transcript ${index + 1} has nothing to type. Fill it in or remove it.`);
|
|
2623
2641
|
}
|
|
2624
2642
|
/** The recording. Its address, description and playback are `checkSharedOptional`'s; the kind and the captions are this type's. */
|
|
2625
|
-
function checkRecording$
|
|
2643
|
+
function checkRecording$2(media) {
|
|
2626
2644
|
if (!isRecord(media)) return [];
|
|
2627
2645
|
const issues = [];
|
|
2628
2646
|
const kind = media.type;
|
|
@@ -3035,6 +3053,7 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3035
3053
|
fib_hint_reveals_answer: "warning",
|
|
3036
3054
|
fib_answer_in_passage: "warning",
|
|
3037
3055
|
fib_accepted_answer_redundant: "advice",
|
|
3056
|
+
fib_tolerance_spans_words: "advice",
|
|
3038
3057
|
gs_title_reveals_answer: "warning",
|
|
3039
3058
|
gs_choices_duplicate: "warning",
|
|
3040
3059
|
gs_bank_no_distractor: "warning",
|
|
@@ -3045,7 +3064,12 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3045
3064
|
media_transcript_unused: "advice",
|
|
3046
3065
|
media_script_differs: "warning",
|
|
3047
3066
|
media_transcript_differs: "warning",
|
|
3048
|
-
set_key_position_same: "warning"
|
|
3067
|
+
set_key_position_same: "warning",
|
|
3068
|
+
set_key_position_bias: "warning",
|
|
3069
|
+
set_key_longest_bias: "warning",
|
|
3070
|
+
set_stem_duplicate: "warning",
|
|
3071
|
+
set_item_answers_item: "warning",
|
|
3072
|
+
set_key_repeated: "advice"
|
|
3049
3073
|
};
|
|
3050
3074
|
function finding(code, path, message) {
|
|
3051
3075
|
return {
|
|
@@ -3235,6 +3259,17 @@ function critiqueFillInTheBlanks(draft) {
|
|
|
3235
3259
|
laterIndex
|
|
3236
3260
|
], `"${laterText}" is already accepted as "${accepted[earlier]}" under this blank's matching.`));
|
|
3237
3261
|
});
|
|
3262
|
+
const forgiven = policyFor(blank).levenshtein ?? 0;
|
|
3263
|
+
const spanning = accepted.findIndex((answer) => {
|
|
3264
|
+
const text = written(answer);
|
|
3265
|
+
return text !== void 0 && /\s/.test(text.trim());
|
|
3266
|
+
});
|
|
3267
|
+
if (forgiven > 0 && spanning !== -1) findings.push(finding("fib_tolerance_spans_words", [
|
|
3268
|
+
"blanks",
|
|
3269
|
+
index,
|
|
3270
|
+
"acceptedAnswers",
|
|
3271
|
+
spanning
|
|
3272
|
+
], `Blank ${ordinal(index)} forgives ${forgiven === 1 ? "one edit" : `${forgiven} edits`} over an answer of several words, so "${accepted[spanning]}" with one word in another form is marked right. Make it a one-word blank, or take the tolerance off.`));
|
|
3238
3273
|
});
|
|
3239
3274
|
return findings;
|
|
3240
3275
|
}
|
|
@@ -4133,8 +4168,8 @@ const readAloudAuthoring = {
|
|
|
4133
4168
|
function checkReadAloudDraft(draft) {
|
|
4134
4169
|
const issues = checkIdentity(draft, "read-aloud");
|
|
4135
4170
|
issues.push(...checkReferenceText(draft.referenceText));
|
|
4136
|
-
issues.push(...checkLocale(draft.locale));
|
|
4137
|
-
issues.push(...checkRecording(draft.recording));
|
|
4171
|
+
issues.push(...checkLocale$1(draft.locale));
|
|
4172
|
+
issues.push(...checkRecording$1(draft.recording));
|
|
4138
4173
|
issues.push(...checkDimensions(draft.scoring));
|
|
4139
4174
|
issues.push(...checkModelRecording(draft.media));
|
|
4140
4175
|
issues.push(...checkSlowRecording(draft));
|
|
@@ -4151,7 +4186,7 @@ function checkReferenceText(referenceText) {
|
|
|
4151
4186
|
if (containsUnspacedScript(referenceText)) issues.push(issue("ra_reference_text_unspaced_script", ["referenceText"], "The text is written in a script that does not separate its words with spaces. Each word is marked on its own, and a run of Han, kana, Thai, Lao, Khmer or Myanmar letters would be marked as one word."));
|
|
4152
4187
|
return issues;
|
|
4153
4188
|
}
|
|
4154
|
-
function checkLocale(locale) {
|
|
4189
|
+
function checkLocale$1(locale) {
|
|
4155
4190
|
if (isUnwritten(locale)) return [issue("ra_locale_required", ["locale"], "Choose the language the learner reads in, with its region — \"en-US\", not \"en\".")];
|
|
4156
4191
|
if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) return [];
|
|
4157
4192
|
return [issue("ra_locale_invalid", ["locale"], "The language must be a BCP 47 tag in canonical form, with a region: \"en-US\", \"es-419\", \"zh-Hant-TW\". Pronunciation is assessed against one locale's speech.")];
|
|
@@ -4161,7 +4196,7 @@ function checkLocale(locale) {
|
|
|
4161
4196
|
* none of them decides a grade, but the longest take has to be set before a
|
|
4162
4197
|
* recorder can be shown at all.
|
|
4163
4198
|
*/
|
|
4164
|
-
function checkRecording(recording) {
|
|
4199
|
+
function checkRecording$1(recording) {
|
|
4165
4200
|
if (isUnset(recording)) return [maxSecondsRequired()];
|
|
4166
4201
|
if (!isRecord(recording)) return [];
|
|
4167
4202
|
const issues = [];
|
|
@@ -4261,6 +4296,217 @@ function checkSlowRecording(draft) {
|
|
|
4261
4296
|
if (typeof slow.url === "string" && slow.url !== "" && slow.url === media.url) issues.push(issue("ra_slow_media_same_recording", ["slowMedia", "url"], "The slow model recording is the same file as the recording. Point it at the slower reading."));
|
|
4262
4297
|
return issues;
|
|
4263
4298
|
}
|
|
4299
|
+
// src/schemas/speaking-response.ts
|
|
4300
|
+
/** The engine measures a speaking criterion can be scored from. */
|
|
4301
|
+
const SPEAKING_MEASURES = [
|
|
4302
|
+
"pronunciation",
|
|
4303
|
+
"fluency",
|
|
4304
|
+
"prosody"
|
|
4305
|
+
];
|
|
4306
|
+
/** One criterion: a written-response criterion, and the measure that may score it. Loose: a sidecar survives. */
|
|
4307
|
+
const SpeakingRubricCriterionSchema = z.looseObject({
|
|
4308
|
+
name: z.string().min(1),
|
|
4309
|
+
description: z.string().optional(),
|
|
4310
|
+
weight: z.number().min(0),
|
|
4311
|
+
measure: z.enum(SPEAKING_MEASURES).optional()
|
|
4312
|
+
});
|
|
4313
|
+
const SpeakingRubricSchema = z.looseObject({
|
|
4314
|
+
label: z.string().optional(),
|
|
4315
|
+
criteria: z.array(SpeakingRubricCriterionSchema).min(1)
|
|
4316
|
+
});
|
|
4317
|
+
/** The limits on a take, as a read-aloud's. Loose, like every content object. */
|
|
4318
|
+
const SpeakingRecordingBoundsSchema = z.looseObject({
|
|
4319
|
+
maxSeconds: z.number().gt(0).max(300),
|
|
4320
|
+
minSeconds: z.number().min(0).optional(),
|
|
4321
|
+
maxTakes: z.number().int().min(1).max(20).optional()
|
|
4322
|
+
});
|
|
4323
|
+
/**
|
|
4324
|
+
* Zod schema validating the full Speaking Response data contract.
|
|
4325
|
+
*
|
|
4326
|
+
* Four semantic guards, none expressible in JSON Schema, each an authoring
|
|
4327
|
+
* error that would otherwise reach a grade:
|
|
4328
|
+
*
|
|
4329
|
+
* 1. **No two criteria share a name** (case and spacing aside): a judgement
|
|
4330
|
+
* names the criterion it scores, and two of one name leave it unsaid which.
|
|
4331
|
+
* 2. **Some criterion carries a weight above 0**, or there is no weighted total
|
|
4332
|
+
* and every answer would be unscorable.
|
|
4333
|
+
* 3. **No measure scores two criteria**: the engine reports one fluency, and
|
|
4334
|
+
* two criteria scored from it would weigh it twice under two names.
|
|
4335
|
+
* 4. **The shortest take is shorter than the longest.**
|
|
4336
|
+
*
|
|
4337
|
+
* The guards run even when an unrelated field was refused, but a guard that
|
|
4338
|
+
* reads a field zod refused waits until that field parses.
|
|
4339
|
+
*/
|
|
4340
|
+
const SpeakingResponseDataSchema = z.looseObject({
|
|
4341
|
+
schemaVersion: z.literal("1.0"),
|
|
4342
|
+
type: z.literal("speaking-response"),
|
|
4343
|
+
id: z.string().min(1),
|
|
4344
|
+
title: z.string().min(1),
|
|
4345
|
+
instructions: z.string().optional(),
|
|
4346
|
+
prompt: z.string(),
|
|
4347
|
+
promptHtml: z.string().optional(),
|
|
4348
|
+
locale: z.string().regex(CANONICAL_LOCALE_RE, { error: "The locale must be a BCP 47 tag in canonical form, with a region: \"en-US\", \"es-419\". Pronunciation is measured against one locale's speech, and \"en\" does not say whose." }),
|
|
4349
|
+
media: MediaSchema.optional(),
|
|
4350
|
+
recording: SpeakingRecordingBoundsSchema,
|
|
4351
|
+
rubric: SpeakingRubricSchema,
|
|
4352
|
+
languageTarget: z.string().optional(),
|
|
4353
|
+
passThreshold: z.number().min(0).max(1).optional(),
|
|
4354
|
+
feedback: FeedbackSchema.optional(),
|
|
4355
|
+
learningObjectives: z.array(z.string()).optional(),
|
|
4356
|
+
difficultyLevel: z.literal([
|
|
4357
|
+
1,
|
|
4358
|
+
2,
|
|
4359
|
+
3,
|
|
4360
|
+
4,
|
|
4361
|
+
5
|
|
4362
|
+
]).optional(),
|
|
4363
|
+
ai: AiPermissionsSchema.optional()
|
|
4364
|
+
}).check(checkEvenAfterIssues((ctx) => {
|
|
4365
|
+
if (ctx.issues.some((issue) => (issue.path?.length ?? 0) === 0)) return;
|
|
4366
|
+
const { data, refused } = parsedFields(ctx);
|
|
4367
|
+
const report = (path, input, message) => {
|
|
4368
|
+
ctx.issues.push({
|
|
4369
|
+
code: "custom",
|
|
4370
|
+
input,
|
|
4371
|
+
message,
|
|
4372
|
+
path: [...path]
|
|
4373
|
+
});
|
|
4374
|
+
};
|
|
4375
|
+
if (!refused.has("rubric")) {
|
|
4376
|
+
const criteria = data.rubric.criteria;
|
|
4377
|
+
const names = /* @__PURE__ */ new Set();
|
|
4378
|
+
const measures = /* @__PURE__ */ new Set();
|
|
4379
|
+
for (const [index, criterion] of criteria.entries()) {
|
|
4380
|
+
const key = levelKey(criterion.name);
|
|
4381
|
+
if (names.has(key)) report([
|
|
4382
|
+
"rubric",
|
|
4383
|
+
"criteria",
|
|
4384
|
+
index,
|
|
4385
|
+
"name"
|
|
4386
|
+
], criterion.name, `Another criterion is already called "${criterion.name}": a judgement names the criterion it scores, and two of one name leave it unsaid which.`);
|
|
4387
|
+
names.add(key);
|
|
4388
|
+
if (criterion.measure !== void 0) {
|
|
4389
|
+
if (measures.has(criterion.measure)) report([
|
|
4390
|
+
"rubric",
|
|
4391
|
+
"criteria",
|
|
4392
|
+
index,
|
|
4393
|
+
"measure"
|
|
4394
|
+
], criterion.measure, `Another criterion is already scored from ${criterion.measure}: the engine reports it once, and two criteria scored from it weigh it twice.`);
|
|
4395
|
+
measures.add(criterion.measure);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
if (criteria.every((criterion) => criterion.weight === 0)) report(["rubric", "criteria"], criteria, "At least one criterion must carry a weight above 0: with every weight at 0 there is no weighted total, and every answer would be unscorable.");
|
|
4399
|
+
}
|
|
4400
|
+
if (!refused.has("recording")) {
|
|
4401
|
+
const { minSeconds, maxSeconds } = data.recording;
|
|
4402
|
+
if (minSeconds !== void 0 && minSeconds >= maxSeconds) report(["recording", "minSeconds"], minSeconds, "The shortest take must be shorter than the longest.");
|
|
4403
|
+
}
|
|
4404
|
+
}));
|
|
4405
|
+
// src/authoring/speaking-response.ts
|
|
4406
|
+
/**
|
|
4407
|
+
* Draft support for `speaking-response`.
|
|
4408
|
+
*
|
|
4409
|
+
* A new draft has no prompt, no locale, no recording time and a rubric with no
|
|
4410
|
+
* criteria: the length of an answer, its language and what it is graded on are
|
|
4411
|
+
* teaching decisions, and the SDK picks none of them. Each is reported
|
|
4412
|
+
* `incomplete` until it is set.
|
|
4413
|
+
*
|
|
4414
|
+
* Stricter than the schema in the places written-response is — a blank plain
|
|
4415
|
+
* prompt beside a rich one, a criterion named only with spaces, weights that
|
|
4416
|
+
* are all 0 or add up past what a number holds — and in the ones read-aloud
|
|
4417
|
+
* is: a locale that is not canonical, bounds outside their range.
|
|
4418
|
+
*/
|
|
4419
|
+
const speakingResponseAuthoring = {
|
|
4420
|
+
createDraft: ({ newId }) => ({
|
|
4421
|
+
schemaVersion: "1.0",
|
|
4422
|
+
type: "speaking-response",
|
|
4423
|
+
id: newId(),
|
|
4424
|
+
title: "",
|
|
4425
|
+
prompt: "",
|
|
4426
|
+
locale: "",
|
|
4427
|
+
recording: { maxSeconds: 0 },
|
|
4428
|
+
rubric: { criteria: [] }
|
|
4429
|
+
}),
|
|
4430
|
+
checkDraft: checkSpeakingResponseDraft
|
|
4431
|
+
};
|
|
4432
|
+
function checkSpeakingResponseDraft(draft) {
|
|
4433
|
+
const issues = checkIdentity(draft, "speaking-response");
|
|
4434
|
+
if (isUnwritten(draft.prompt)) issues.push(issue("sr_prompt_required", ["prompt"], isUnwritten(draft.promptHtml) ? "Write the prompt the learner answers." : "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."));
|
|
4435
|
+
issues.push(...checkLocale(draft.locale));
|
|
4436
|
+
issues.push(...checkRecording(draft.recording));
|
|
4437
|
+
issues.push(...checkRubric(draft.rubric));
|
|
4438
|
+
issues.push(...checkSharedOptional(draft));
|
|
4439
|
+
return issues;
|
|
4440
|
+
}
|
|
4441
|
+
function checkLocale(locale) {
|
|
4442
|
+
if (isUnwritten(locale)) return [issue("sr_locale_required", ["locale"], "Choose the language the learner answers in, with its region — \"en-US\", not \"en\".")];
|
|
4443
|
+
if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) return [];
|
|
4444
|
+
return [issue("sr_locale_invalid", ["locale"], "The language must be a BCP 47 tag in canonical form, with a region: \"en-US\", \"es-419\". Pronunciation is measured against one locale's speech.")];
|
|
4445
|
+
}
|
|
4446
|
+
function checkRecording(recording) {
|
|
4447
|
+
const required = issue("sr_max_seconds_required", ["recording", "maxSeconds"], "Set how long an answer may be, in seconds.");
|
|
4448
|
+
if (isUnset(recording)) return [required];
|
|
4449
|
+
if (!isRecord(recording)) return [];
|
|
4450
|
+
const issues = [];
|
|
4451
|
+
const max = recording.maxSeconds;
|
|
4452
|
+
if (isUnset(max) || max === 0) issues.push(required);
|
|
4453
|
+
else if (typeof max === "number" && !(Number.isFinite(max) && max > 0 && max <= 300)) issues.push(issue("sr_max_seconds_out_of_range", ["recording", "maxSeconds"], `An answer is at most 300 seconds long.`));
|
|
4454
|
+
const min = recording.minSeconds;
|
|
4455
|
+
const minOut = (message) => issue("sr_min_seconds_out_of_range", ["recording", "minSeconds"], message);
|
|
4456
|
+
if (typeof min === "number" && !(Number.isFinite(min) && min >= 0)) issues.push(minOut("The shortest answer is a number of seconds, 0 or more."));
|
|
4457
|
+
else if (typeof min === "number" && typeof max === "number" && max > 0 && min >= max) issues.push(minOut("The shortest answer must be shorter than the longest."));
|
|
4458
|
+
const takes = recording.maxTakes;
|
|
4459
|
+
if (typeof takes === "number" && !(isWholeNumber(takes) && takes >= 1 && takes <= 20)) issues.push(issue("sr_max_takes_out_of_range", ["recording", "maxTakes"], `How many takes a learner may record is a whole number from 1 to 20.`));
|
|
4460
|
+
return issues;
|
|
4461
|
+
}
|
|
4462
|
+
const MEASURES = SPEAKING_MEASURES;
|
|
4463
|
+
function checkRubric(rubric) {
|
|
4464
|
+
if (isUnset(rubric)) return [issue("sr_rubric_required", ["rubric"], "Add the rubric the answer is graded on.")];
|
|
4465
|
+
if (!isRecord(rubric)) return [];
|
|
4466
|
+
const criteria = isUnset(rubric.criteria) ? [] : rubric.criteria;
|
|
4467
|
+
if (!Array.isArray(criteria)) return [];
|
|
4468
|
+
if (criteria.length === 0) return [issue("sr_rubric_criteria_required", ["rubric", "criteria"], "Add at least one rubric criterion: what the answer is graded on.")];
|
|
4469
|
+
const issues = [];
|
|
4470
|
+
const names = /* @__PURE__ */ new Set();
|
|
4471
|
+
const measures = /* @__PURE__ */ new Set();
|
|
4472
|
+
let totalWeight = 0;
|
|
4473
|
+
let everyWeightUsable = true;
|
|
4474
|
+
for (const [index, criterion] of criteria.entries()) {
|
|
4475
|
+
if (!isRecord(criterion)) {
|
|
4476
|
+
everyWeightUsable = false;
|
|
4477
|
+
continue;
|
|
4478
|
+
}
|
|
4479
|
+
const ordinal = index + 1;
|
|
4480
|
+
const path = [
|
|
4481
|
+
"rubric",
|
|
4482
|
+
"criteria",
|
|
4483
|
+
index
|
|
4484
|
+
];
|
|
4485
|
+
if (isUnwritten(criterion.name)) issues.push(issue("sr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`));
|
|
4486
|
+
else if (typeof criterion.name === "string") {
|
|
4487
|
+
const key = levelKey(criterion.name);
|
|
4488
|
+
if (names.has(key)) issues.push(issue("sr_criterion_name_duplicate", [...path, "name"], `Another criterion is already called "${criterion.name}". A judgement names the criterion it scores, so names must differ.`));
|
|
4489
|
+
names.add(key);
|
|
4490
|
+
}
|
|
4491
|
+
const weight = criterion.weight;
|
|
4492
|
+
if (isUnset(weight)) {
|
|
4493
|
+
everyWeightUsable = false;
|
|
4494
|
+
issues.push(issue("sr_criterion_weight_required", [...path, "weight"], `Give rubric criterion ${ordinal} a weight.`));
|
|
4495
|
+
} else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
|
|
4496
|
+
everyWeightUsable = false;
|
|
4497
|
+
issues.push(issue("sr_criterion_weight_invalid", [...path, "weight"], "A rubric weight must be a number, 0 or more."));
|
|
4498
|
+
} else totalWeight += weight;
|
|
4499
|
+
const measure = criterion.measure;
|
|
4500
|
+
if (measure !== void 0) {
|
|
4501
|
+
if (!MEASURES.includes(measure)) issues.push(issue("sr_criterion_measure_invalid", [...path, "measure"], `A criterion is scored from ${SPEAKING_MEASURES.join(", ")}, or by the judge when it names no measure.`));
|
|
4502
|
+
else if (measures.has(measure)) issues.push(issue("sr_criterion_measure_duplicate", [...path, "measure"], `Another criterion is already scored from ${String(measure)}: the engine reports it once, and two criteria scored from it would weigh it twice.`));
|
|
4503
|
+
measures.add(measure);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
if (everyWeightUsable && totalWeight === 0) issues.push(issue("sr_rubric_weights_zero", ["rubric", "criteria"], "Give at least one rubric criterion a weight above 0. With every weight at 0, no weighted total can be computed."));
|
|
4507
|
+
else if (totalWeight === Number.POSITIVE_INFINITY) issues.push(issue("sr_rubric_weights_too_large", ["rubric", "criteria"], "The rubric weights add up to more than can be calculated with. Use smaller weights."));
|
|
4508
|
+
return issues;
|
|
4509
|
+
}
|
|
4264
4510
|
// src/authoring/written-response.ts
|
|
4265
4511
|
/**
|
|
4266
4512
|
* Draft support for `written-response`.
|
|
@@ -4790,6 +5036,34 @@ const RedactedReadAloudDataSchema = z.strictObject({
|
|
|
4790
5036
|
path: ["slowMedia", "url"]
|
|
4791
5037
|
});
|
|
4792
5038
|
});
|
|
5039
|
+
/**
|
|
5040
|
+
* Redacted Speaking Response data: everything a learner needs to answer — the
|
|
5041
|
+
* prompt, the picture or recording, the limits of a take — and the rubric, which
|
|
5042
|
+
* tells them what the answer is graded on, as a written response's does (a
|
|
5043
|
+
* deployment may hide it per call). Only the authored pass/fail feedback is
|
|
5044
|
+
* removed: it is written about a grade that does not exist yet.
|
|
5045
|
+
*/
|
|
5046
|
+
const RedactedSpeakingResponseDataSchema = z.strictObject({
|
|
5047
|
+
...redactedBase,
|
|
5048
|
+
type: z.literal("speaking-response"),
|
|
5049
|
+
locale: z.string().regex(CANONICAL_LOCALE_RE),
|
|
5050
|
+
instructions: z.string().optional(),
|
|
5051
|
+
prompt: z.string(),
|
|
5052
|
+
promptHtml: z.string().optional(),
|
|
5053
|
+
recording: z.strictObject({
|
|
5054
|
+
maxSeconds: z.number().gt(0).max(300),
|
|
5055
|
+
minSeconds: z.number().min(0).optional(),
|
|
5056
|
+
maxTakes: z.number().int().min(1).max(20).optional()
|
|
5057
|
+
}),
|
|
5058
|
+
rubric: z.strictObject({
|
|
5059
|
+
label: z.string().optional(),
|
|
5060
|
+
criteria: z.array(z.strictObject({
|
|
5061
|
+
...RedactedWrittenResponseRubricCriterionSchema.shape,
|
|
5062
|
+
measure: z.enum(SPEAKING_MEASURES).optional()
|
|
5063
|
+
})).min(1)
|
|
5064
|
+
}).optional(),
|
|
5065
|
+
languageTarget: z.string().optional()
|
|
5066
|
+
});
|
|
4793
5067
|
// src/scoring/activity-scorers/dictation.ts
|
|
4794
5068
|
/**
|
|
4795
5069
|
* Scores a Dictation response: the character-level similarity of what the
|
|
@@ -5394,6 +5668,34 @@ const READ_ALOUD_FIELD_POLICY = {
|
|
|
5394
5668
|
feedback: FEEDBACK_FIELD_POLICY
|
|
5395
5669
|
};
|
|
5396
5670
|
/**
|
|
5671
|
+
* Speaking Response: public, as written-response is. The task, the picture and
|
|
5672
|
+
* the limits of a take are what the learner needs to answer; the rubric tells
|
|
5673
|
+
* them what the answer is graded on — classified key by key, a criterion's
|
|
5674
|
+
* `measure` included, so nothing parked on it reaches a learner. A deployment
|
|
5675
|
+
* that wants the rubric hidden tightens it per call: the redacted schema leaves
|
|
5676
|
+
* it optional. Only the authored pass/fail feedback is removed.
|
|
5677
|
+
*/
|
|
5678
|
+
const SPEAKING_RESPONSE_FIELD_POLICY = {
|
|
5679
|
+
...SHARED_PUBLIC_FIELDS,
|
|
5680
|
+
instructions: "public",
|
|
5681
|
+
prompt: "public",
|
|
5682
|
+
promptHtml: "public",
|
|
5683
|
+
recording: {
|
|
5684
|
+
maxSeconds: "public",
|
|
5685
|
+
minSeconds: "public",
|
|
5686
|
+
maxTakes: "public"
|
|
5687
|
+
},
|
|
5688
|
+
languageTarget: "public",
|
|
5689
|
+
feedback: FEEDBACK_FIELD_POLICY,
|
|
5690
|
+
rubric: {
|
|
5691
|
+
...RUBRIC_FIELD_POLICY,
|
|
5692
|
+
criteria: {
|
|
5693
|
+
...RUBRIC_FIELD_POLICY.criteria,
|
|
5694
|
+
measure: "public"
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
};
|
|
5698
|
+
/**
|
|
5397
5699
|
* Whether a response carries a stored recording: an object whose `key` is a
|
|
5398
5700
|
* non-empty string. Read defensively, because both callers are handed whatever
|
|
5399
5701
|
* an application stored — `evaluate()` passes `partial` a response that may be
|
|
@@ -5448,7 +5750,32 @@ const BUILT_IN_DESCRIPTORS = {
|
|
|
5448
5750
|
"written-response": writtenResponseType,
|
|
5449
5751
|
"gap-select": gapSelectType,
|
|
5450
5752
|
dictation: dictationType,
|
|
5451
|
-
"read-aloud": readAloudType
|
|
5753
|
+
"read-aloud": readAloudType,
|
|
5754
|
+
"speaking-response": defineActivityType({
|
|
5755
|
+
type: "speaking-response",
|
|
5756
|
+
schema: SpeakingResponseDataSchema,
|
|
5757
|
+
scoring: {
|
|
5758
|
+
kind: "deferred",
|
|
5759
|
+
reason: "requires_async_grading",
|
|
5760
|
+
partial: (_data, response) => ({ hasRecording: hasRecording(response) })
|
|
5761
|
+
},
|
|
5762
|
+
isAnswered: (response) => hasRecording(response),
|
|
5763
|
+
fieldPolicy: SPEAKING_RESPONSE_FIELD_POLICY,
|
|
5764
|
+
redactedSchema: RedactedSpeakingResponseDataSchema,
|
|
5765
|
+
interop: {
|
|
5766
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
5767
|
+
xapiInteractionType: "other",
|
|
5768
|
+
correctResponsesPattern: () => []
|
|
5769
|
+
},
|
|
5770
|
+
interactions: [
|
|
5771
|
+
"recording-started",
|
|
5772
|
+
"recording-stopped",
|
|
5773
|
+
"recording-uploaded",
|
|
5774
|
+
"submitted"
|
|
5775
|
+
],
|
|
5776
|
+
authoring: speakingResponseAuthoring,
|
|
5777
|
+
mediaSlots: () => MAIN_MEDIA
|
|
5778
|
+
})
|
|
5452
5779
|
};
|
|
5453
5780
|
// src/schemas/read-schema.ts
|
|
5454
5781
|
/**
|
|
@@ -5612,6 +5939,6 @@ function getActivityTypeDescriptor(type) {
|
|
|
5612
5939
|
function registeredActivityTypes() {
|
|
5613
5940
|
return [...registry.keys()];
|
|
5614
5941
|
}
|
|
5615
|
-
export {
|
|
5942
|
+
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 };
|
|
5616
5943
|
|
|
5617
|
-
//# sourceMappingURL=registry-
|
|
5944
|
+
//# sourceMappingURL=registry-Bw9grsCN.js.map
|