@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
|
@@ -463,7 +463,8 @@ const BUILT_IN_ACTIVITY_TYPES = [
|
|
|
463
463
|
"written-response",
|
|
464
464
|
"gap-select",
|
|
465
465
|
"dictation",
|
|
466
|
-
"read-aloud"
|
|
466
|
+
"read-aloud",
|
|
467
|
+
"speaking-response"
|
|
467
468
|
];
|
|
468
469
|
/** Whether `type` names an activity type the SDK ships. */
|
|
469
470
|
function isBuiltInActivityType(type) {
|
|
@@ -2371,7 +2372,24 @@ const DRAFT_ISSUE_SEVERITY = {
|
|
|
2371
2372
|
ra_media_kind: "invalid",
|
|
2372
2373
|
ra_slow_media_without_media: "incomplete",
|
|
2373
2374
|
ra_slow_media_same_recording: "invalid",
|
|
2374
|
-
ra_slow_media_beside_play_limit: "invalid"
|
|
2375
|
+
ra_slow_media_beside_play_limit: "invalid",
|
|
2376
|
+
sr_prompt_required: "incomplete",
|
|
2377
|
+
sr_locale_required: "incomplete",
|
|
2378
|
+
sr_locale_invalid: "invalid",
|
|
2379
|
+
sr_max_seconds_required: "incomplete",
|
|
2380
|
+
sr_max_seconds_out_of_range: "invalid",
|
|
2381
|
+
sr_min_seconds_out_of_range: "invalid",
|
|
2382
|
+
sr_max_takes_out_of_range: "invalid",
|
|
2383
|
+
sr_rubric_required: "incomplete",
|
|
2384
|
+
sr_rubric_criteria_required: "incomplete",
|
|
2385
|
+
sr_criterion_name_required: "incomplete",
|
|
2386
|
+
sr_criterion_name_duplicate: "invalid",
|
|
2387
|
+
sr_criterion_weight_required: "incomplete",
|
|
2388
|
+
sr_criterion_weight_invalid: "invalid",
|
|
2389
|
+
sr_criterion_measure_invalid: "invalid",
|
|
2390
|
+
sr_criterion_measure_duplicate: "invalid",
|
|
2391
|
+
sr_rubric_weights_zero: "incomplete",
|
|
2392
|
+
sr_rubric_weights_too_large: "invalid"
|
|
2375
2393
|
};
|
|
2376
2394
|
function issue(code, path, message) {
|
|
2377
2395
|
return {
|
|
@@ -2586,7 +2604,7 @@ function checkDictationDraft(draft) {
|
|
|
2586
2604
|
const normalizer = dictationNormalizer(isRecord(draft.tolerance) ? draft.tolerance : void 0);
|
|
2587
2605
|
issues.push(...checkTranscript(draft.transcript, normalizer));
|
|
2588
2606
|
issues.push(...checkAcceptedTranscripts(draft, normalizer));
|
|
2589
|
-
issues.push(...checkRecording$
|
|
2607
|
+
issues.push(...checkRecording$2(draft.media));
|
|
2590
2608
|
issues.push(...checkSlowRecording$1(draft));
|
|
2591
2609
|
issues.push(...checkRevealed(draft, normalizer));
|
|
2592
2610
|
issues.push(...checkHints(draft.hints));
|
|
@@ -2644,7 +2662,7 @@ function acceptedTranscriptEmpty(index) {
|
|
|
2644
2662
|
return issue("dc_accepted_transcript_empty", ["acceptedTranscripts", index], `Accepted transcript ${index + 1} has nothing to type. Fill it in or remove it.`);
|
|
2645
2663
|
}
|
|
2646
2664
|
/** The recording. Its address, description and playback are `checkSharedOptional`'s; the kind and the captions are this type's. */
|
|
2647
|
-
function checkRecording$
|
|
2665
|
+
function checkRecording$2(media) {
|
|
2648
2666
|
if (!isRecord(media)) return [];
|
|
2649
2667
|
const issues = [];
|
|
2650
2668
|
const kind = media.type;
|
|
@@ -3057,6 +3075,7 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3057
3075
|
fib_hint_reveals_answer: "warning",
|
|
3058
3076
|
fib_answer_in_passage: "warning",
|
|
3059
3077
|
fib_accepted_answer_redundant: "advice",
|
|
3078
|
+
fib_tolerance_spans_words: "advice",
|
|
3060
3079
|
gs_title_reveals_answer: "warning",
|
|
3061
3080
|
gs_choices_duplicate: "warning",
|
|
3062
3081
|
gs_bank_no_distractor: "warning",
|
|
@@ -3067,7 +3086,12 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3067
3086
|
media_transcript_unused: "advice",
|
|
3068
3087
|
media_script_differs: "warning",
|
|
3069
3088
|
media_transcript_differs: "warning",
|
|
3070
|
-
set_key_position_same: "warning"
|
|
3089
|
+
set_key_position_same: "warning",
|
|
3090
|
+
set_key_position_bias: "warning",
|
|
3091
|
+
set_key_longest_bias: "warning",
|
|
3092
|
+
set_stem_duplicate: "warning",
|
|
3093
|
+
set_item_answers_item: "warning",
|
|
3094
|
+
set_key_repeated: "advice"
|
|
3071
3095
|
};
|
|
3072
3096
|
function finding(code, path, message) {
|
|
3073
3097
|
return {
|
|
@@ -3257,6 +3281,17 @@ function critiqueFillInTheBlanks(draft) {
|
|
|
3257
3281
|
laterIndex
|
|
3258
3282
|
], `"${laterText}" is already accepted as "${accepted[earlier]}" under this blank's matching.`));
|
|
3259
3283
|
});
|
|
3284
|
+
const forgiven = policyFor(blank).levenshtein ?? 0;
|
|
3285
|
+
const spanning = accepted.findIndex((answer) => {
|
|
3286
|
+
const text = written(answer);
|
|
3287
|
+
return text !== void 0 && /\s/.test(text.trim());
|
|
3288
|
+
});
|
|
3289
|
+
if (forgiven > 0 && spanning !== -1) findings.push(finding("fib_tolerance_spans_words", [
|
|
3290
|
+
"blanks",
|
|
3291
|
+
index,
|
|
3292
|
+
"acceptedAnswers",
|
|
3293
|
+
spanning
|
|
3294
|
+
], `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.`));
|
|
3260
3295
|
});
|
|
3261
3296
|
return findings;
|
|
3262
3297
|
}
|
|
@@ -4155,8 +4190,8 @@ const readAloudAuthoring = {
|
|
|
4155
4190
|
function checkReadAloudDraft(draft) {
|
|
4156
4191
|
const issues = checkIdentity(draft, "read-aloud");
|
|
4157
4192
|
issues.push(...checkReferenceText(draft.referenceText));
|
|
4158
|
-
issues.push(...checkLocale(draft.locale));
|
|
4159
|
-
issues.push(...checkRecording(draft.recording));
|
|
4193
|
+
issues.push(...checkLocale$1(draft.locale));
|
|
4194
|
+
issues.push(...checkRecording$1(draft.recording));
|
|
4160
4195
|
issues.push(...checkDimensions(draft.scoring));
|
|
4161
4196
|
issues.push(...checkModelRecording(draft.media));
|
|
4162
4197
|
issues.push(...checkSlowRecording(draft));
|
|
@@ -4173,7 +4208,7 @@ function checkReferenceText(referenceText) {
|
|
|
4173
4208
|
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."));
|
|
4174
4209
|
return issues;
|
|
4175
4210
|
}
|
|
4176
|
-
function checkLocale(locale) {
|
|
4211
|
+
function checkLocale$1(locale) {
|
|
4177
4212
|
if (isUnwritten(locale)) return [issue("ra_locale_required", ["locale"], "Choose the language the learner reads in, with its region — \"en-US\", not \"en\".")];
|
|
4178
4213
|
if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) return [];
|
|
4179
4214
|
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.")];
|
|
@@ -4183,7 +4218,7 @@ function checkLocale(locale) {
|
|
|
4183
4218
|
* none of them decides a grade, but the longest take has to be set before a
|
|
4184
4219
|
* recorder can be shown at all.
|
|
4185
4220
|
*/
|
|
4186
|
-
function checkRecording(recording) {
|
|
4221
|
+
function checkRecording$1(recording) {
|
|
4187
4222
|
if (isUnset(recording)) return [maxSecondsRequired()];
|
|
4188
4223
|
if (!isRecord(recording)) return [];
|
|
4189
4224
|
const issues = [];
|
|
@@ -4283,6 +4318,217 @@ function checkSlowRecording(draft) {
|
|
|
4283
4318
|
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."));
|
|
4284
4319
|
return issues;
|
|
4285
4320
|
}
|
|
4321
|
+
// src/schemas/speaking-response.ts
|
|
4322
|
+
/** The engine measures a speaking criterion can be scored from. */
|
|
4323
|
+
const SPEAKING_MEASURES = [
|
|
4324
|
+
"pronunciation",
|
|
4325
|
+
"fluency",
|
|
4326
|
+
"prosody"
|
|
4327
|
+
];
|
|
4328
|
+
/** One criterion: a written-response criterion, and the measure that may score it. Loose: a sidecar survives. */
|
|
4329
|
+
const SpeakingRubricCriterionSchema = zod_v4.looseObject({
|
|
4330
|
+
name: zod_v4.string().min(1),
|
|
4331
|
+
description: zod_v4.string().optional(),
|
|
4332
|
+
weight: zod_v4.number().min(0),
|
|
4333
|
+
measure: zod_v4.enum(SPEAKING_MEASURES).optional()
|
|
4334
|
+
});
|
|
4335
|
+
const SpeakingRubricSchema = zod_v4.looseObject({
|
|
4336
|
+
label: zod_v4.string().optional(),
|
|
4337
|
+
criteria: zod_v4.array(SpeakingRubricCriterionSchema).min(1)
|
|
4338
|
+
});
|
|
4339
|
+
/** The limits on a take, as a read-aloud's. Loose, like every content object. */
|
|
4340
|
+
const SpeakingRecordingBoundsSchema = zod_v4.looseObject({
|
|
4341
|
+
maxSeconds: zod_v4.number().gt(0).max(300),
|
|
4342
|
+
minSeconds: zod_v4.number().min(0).optional(),
|
|
4343
|
+
maxTakes: zod_v4.number().int().min(1).max(20).optional()
|
|
4344
|
+
});
|
|
4345
|
+
/**
|
|
4346
|
+
* Zod schema validating the full Speaking Response data contract.
|
|
4347
|
+
*
|
|
4348
|
+
* Four semantic guards, none expressible in JSON Schema, each an authoring
|
|
4349
|
+
* error that would otherwise reach a grade:
|
|
4350
|
+
*
|
|
4351
|
+
* 1. **No two criteria share a name** (case and spacing aside): a judgement
|
|
4352
|
+
* names the criterion it scores, and two of one name leave it unsaid which.
|
|
4353
|
+
* 2. **Some criterion carries a weight above 0**, or there is no weighted total
|
|
4354
|
+
* and every answer would be unscorable.
|
|
4355
|
+
* 3. **No measure scores two criteria**: the engine reports one fluency, and
|
|
4356
|
+
* two criteria scored from it would weigh it twice under two names.
|
|
4357
|
+
* 4. **The shortest take is shorter than the longest.**
|
|
4358
|
+
*
|
|
4359
|
+
* The guards run even when an unrelated field was refused, but a guard that
|
|
4360
|
+
* reads a field zod refused waits until that field parses.
|
|
4361
|
+
*/
|
|
4362
|
+
const SpeakingResponseDataSchema = zod_v4.looseObject({
|
|
4363
|
+
schemaVersion: zod_v4.literal("1.0"),
|
|
4364
|
+
type: zod_v4.literal("speaking-response"),
|
|
4365
|
+
id: zod_v4.string().min(1),
|
|
4366
|
+
title: zod_v4.string().min(1),
|
|
4367
|
+
instructions: zod_v4.string().optional(),
|
|
4368
|
+
prompt: zod_v4.string(),
|
|
4369
|
+
promptHtml: zod_v4.string().optional(),
|
|
4370
|
+
locale: zod_v4.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." }),
|
|
4371
|
+
media: MediaSchema.optional(),
|
|
4372
|
+
recording: SpeakingRecordingBoundsSchema,
|
|
4373
|
+
rubric: SpeakingRubricSchema,
|
|
4374
|
+
languageTarget: zod_v4.string().optional(),
|
|
4375
|
+
passThreshold: zod_v4.number().min(0).max(1).optional(),
|
|
4376
|
+
feedback: FeedbackSchema.optional(),
|
|
4377
|
+
learningObjectives: zod_v4.array(zod_v4.string()).optional(),
|
|
4378
|
+
difficultyLevel: zod_v4.literal([
|
|
4379
|
+
1,
|
|
4380
|
+
2,
|
|
4381
|
+
3,
|
|
4382
|
+
4,
|
|
4383
|
+
5
|
|
4384
|
+
]).optional(),
|
|
4385
|
+
ai: AiPermissionsSchema.optional()
|
|
4386
|
+
}).check(checkEvenAfterIssues((ctx) => {
|
|
4387
|
+
if (ctx.issues.some((issue) => (issue.path?.length ?? 0) === 0)) return;
|
|
4388
|
+
const { data, refused } = parsedFields(ctx);
|
|
4389
|
+
const report = (path, input, message) => {
|
|
4390
|
+
ctx.issues.push({
|
|
4391
|
+
code: "custom",
|
|
4392
|
+
input,
|
|
4393
|
+
message,
|
|
4394
|
+
path: [...path]
|
|
4395
|
+
});
|
|
4396
|
+
};
|
|
4397
|
+
if (!refused.has("rubric")) {
|
|
4398
|
+
const criteria = data.rubric.criteria;
|
|
4399
|
+
const names = /* @__PURE__ */ new Set();
|
|
4400
|
+
const measures = /* @__PURE__ */ new Set();
|
|
4401
|
+
for (const [index, criterion] of criteria.entries()) {
|
|
4402
|
+
const key = levelKey(criterion.name);
|
|
4403
|
+
if (names.has(key)) report([
|
|
4404
|
+
"rubric",
|
|
4405
|
+
"criteria",
|
|
4406
|
+
index,
|
|
4407
|
+
"name"
|
|
4408
|
+
], 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.`);
|
|
4409
|
+
names.add(key);
|
|
4410
|
+
if (criterion.measure !== void 0) {
|
|
4411
|
+
if (measures.has(criterion.measure)) report([
|
|
4412
|
+
"rubric",
|
|
4413
|
+
"criteria",
|
|
4414
|
+
index,
|
|
4415
|
+
"measure"
|
|
4416
|
+
], criterion.measure, `Another criterion is already scored from ${criterion.measure}: the engine reports it once, and two criteria scored from it weigh it twice.`);
|
|
4417
|
+
measures.add(criterion.measure);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
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.");
|
|
4421
|
+
}
|
|
4422
|
+
if (!refused.has("recording")) {
|
|
4423
|
+
const { minSeconds, maxSeconds } = data.recording;
|
|
4424
|
+
if (minSeconds !== void 0 && minSeconds >= maxSeconds) report(["recording", "minSeconds"], minSeconds, "The shortest take must be shorter than the longest.");
|
|
4425
|
+
}
|
|
4426
|
+
}));
|
|
4427
|
+
// src/authoring/speaking-response.ts
|
|
4428
|
+
/**
|
|
4429
|
+
* Draft support for `speaking-response`.
|
|
4430
|
+
*
|
|
4431
|
+
* A new draft has no prompt, no locale, no recording time and a rubric with no
|
|
4432
|
+
* criteria: the length of an answer, its language and what it is graded on are
|
|
4433
|
+
* teaching decisions, and the SDK picks none of them. Each is reported
|
|
4434
|
+
* `incomplete` until it is set.
|
|
4435
|
+
*
|
|
4436
|
+
* Stricter than the schema in the places written-response is — a blank plain
|
|
4437
|
+
* prompt beside a rich one, a criterion named only with spaces, weights that
|
|
4438
|
+
* are all 0 or add up past what a number holds — and in the ones read-aloud
|
|
4439
|
+
* is: a locale that is not canonical, bounds outside their range.
|
|
4440
|
+
*/
|
|
4441
|
+
const speakingResponseAuthoring = {
|
|
4442
|
+
createDraft: ({ newId }) => ({
|
|
4443
|
+
schemaVersion: "1.0",
|
|
4444
|
+
type: "speaking-response",
|
|
4445
|
+
id: newId(),
|
|
4446
|
+
title: "",
|
|
4447
|
+
prompt: "",
|
|
4448
|
+
locale: "",
|
|
4449
|
+
recording: { maxSeconds: 0 },
|
|
4450
|
+
rubric: { criteria: [] }
|
|
4451
|
+
}),
|
|
4452
|
+
checkDraft: checkSpeakingResponseDraft
|
|
4453
|
+
};
|
|
4454
|
+
function checkSpeakingResponseDraft(draft) {
|
|
4455
|
+
const issues = checkIdentity(draft, "speaking-response");
|
|
4456
|
+
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."));
|
|
4457
|
+
issues.push(...checkLocale(draft.locale));
|
|
4458
|
+
issues.push(...checkRecording(draft.recording));
|
|
4459
|
+
issues.push(...checkRubric(draft.rubric));
|
|
4460
|
+
issues.push(...checkSharedOptional(draft));
|
|
4461
|
+
return issues;
|
|
4462
|
+
}
|
|
4463
|
+
function checkLocale(locale) {
|
|
4464
|
+
if (isUnwritten(locale)) return [issue("sr_locale_required", ["locale"], "Choose the language the learner answers in, with its region — \"en-US\", not \"en\".")];
|
|
4465
|
+
if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) return [];
|
|
4466
|
+
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.")];
|
|
4467
|
+
}
|
|
4468
|
+
function checkRecording(recording) {
|
|
4469
|
+
const required = issue("sr_max_seconds_required", ["recording", "maxSeconds"], "Set how long an answer may be, in seconds.");
|
|
4470
|
+
if (isUnset(recording)) return [required];
|
|
4471
|
+
if (!isRecord(recording)) return [];
|
|
4472
|
+
const issues = [];
|
|
4473
|
+
const max = recording.maxSeconds;
|
|
4474
|
+
if (isUnset(max) || max === 0) issues.push(required);
|
|
4475
|
+
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.`));
|
|
4476
|
+
const min = recording.minSeconds;
|
|
4477
|
+
const minOut = (message) => issue("sr_min_seconds_out_of_range", ["recording", "minSeconds"], message);
|
|
4478
|
+
if (typeof min === "number" && !(Number.isFinite(min) && min >= 0)) issues.push(minOut("The shortest answer is a number of seconds, 0 or more."));
|
|
4479
|
+
else if (typeof min === "number" && typeof max === "number" && max > 0 && min >= max) issues.push(minOut("The shortest answer must be shorter than the longest."));
|
|
4480
|
+
const takes = recording.maxTakes;
|
|
4481
|
+
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.`));
|
|
4482
|
+
return issues;
|
|
4483
|
+
}
|
|
4484
|
+
const MEASURES = SPEAKING_MEASURES;
|
|
4485
|
+
function checkRubric(rubric) {
|
|
4486
|
+
if (isUnset(rubric)) return [issue("sr_rubric_required", ["rubric"], "Add the rubric the answer is graded on.")];
|
|
4487
|
+
if (!isRecord(rubric)) return [];
|
|
4488
|
+
const criteria = isUnset(rubric.criteria) ? [] : rubric.criteria;
|
|
4489
|
+
if (!Array.isArray(criteria)) return [];
|
|
4490
|
+
if (criteria.length === 0) return [issue("sr_rubric_criteria_required", ["rubric", "criteria"], "Add at least one rubric criterion: what the answer is graded on.")];
|
|
4491
|
+
const issues = [];
|
|
4492
|
+
const names = /* @__PURE__ */ new Set();
|
|
4493
|
+
const measures = /* @__PURE__ */ new Set();
|
|
4494
|
+
let totalWeight = 0;
|
|
4495
|
+
let everyWeightUsable = true;
|
|
4496
|
+
for (const [index, criterion] of criteria.entries()) {
|
|
4497
|
+
if (!isRecord(criterion)) {
|
|
4498
|
+
everyWeightUsable = false;
|
|
4499
|
+
continue;
|
|
4500
|
+
}
|
|
4501
|
+
const ordinal = index + 1;
|
|
4502
|
+
const path = [
|
|
4503
|
+
"rubric",
|
|
4504
|
+
"criteria",
|
|
4505
|
+
index
|
|
4506
|
+
];
|
|
4507
|
+
if (isUnwritten(criterion.name)) issues.push(issue("sr_criterion_name_required", [...path, "name"], `Name rubric criterion ${ordinal}.`));
|
|
4508
|
+
else if (typeof criterion.name === "string") {
|
|
4509
|
+
const key = levelKey(criterion.name);
|
|
4510
|
+
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.`));
|
|
4511
|
+
names.add(key);
|
|
4512
|
+
}
|
|
4513
|
+
const weight = criterion.weight;
|
|
4514
|
+
if (isUnset(weight)) {
|
|
4515
|
+
everyWeightUsable = false;
|
|
4516
|
+
issues.push(issue("sr_criterion_weight_required", [...path, "weight"], `Give rubric criterion ${ordinal} a weight.`));
|
|
4517
|
+
} else if (!(typeof weight === "number" && Number.isFinite(weight) && weight >= 0)) {
|
|
4518
|
+
everyWeightUsable = false;
|
|
4519
|
+
issues.push(issue("sr_criterion_weight_invalid", [...path, "weight"], "A rubric weight must be a number, 0 or more."));
|
|
4520
|
+
} else totalWeight += weight;
|
|
4521
|
+
const measure = criterion.measure;
|
|
4522
|
+
if (measure !== void 0) {
|
|
4523
|
+
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.`));
|
|
4524
|
+
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.`));
|
|
4525
|
+
measures.add(measure);
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
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."));
|
|
4529
|
+
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."));
|
|
4530
|
+
return issues;
|
|
4531
|
+
}
|
|
4286
4532
|
// src/authoring/written-response.ts
|
|
4287
4533
|
/**
|
|
4288
4534
|
* Draft support for `written-response`.
|
|
@@ -4812,6 +5058,34 @@ const RedactedReadAloudDataSchema = zod_v4.strictObject({
|
|
|
4812
5058
|
path: ["slowMedia", "url"]
|
|
4813
5059
|
});
|
|
4814
5060
|
});
|
|
5061
|
+
/**
|
|
5062
|
+
* Redacted Speaking Response data: everything a learner needs to answer — the
|
|
5063
|
+
* prompt, the picture or recording, the limits of a take — and the rubric, which
|
|
5064
|
+
* tells them what the answer is graded on, as a written response's does (a
|
|
5065
|
+
* deployment may hide it per call). Only the authored pass/fail feedback is
|
|
5066
|
+
* removed: it is written about a grade that does not exist yet.
|
|
5067
|
+
*/
|
|
5068
|
+
const RedactedSpeakingResponseDataSchema = zod_v4.strictObject({
|
|
5069
|
+
...redactedBase,
|
|
5070
|
+
type: zod_v4.literal("speaking-response"),
|
|
5071
|
+
locale: zod_v4.string().regex(CANONICAL_LOCALE_RE),
|
|
5072
|
+
instructions: zod_v4.string().optional(),
|
|
5073
|
+
prompt: zod_v4.string(),
|
|
5074
|
+
promptHtml: zod_v4.string().optional(),
|
|
5075
|
+
recording: zod_v4.strictObject({
|
|
5076
|
+
maxSeconds: zod_v4.number().gt(0).max(300),
|
|
5077
|
+
minSeconds: zod_v4.number().min(0).optional(),
|
|
5078
|
+
maxTakes: zod_v4.number().int().min(1).max(20).optional()
|
|
5079
|
+
}),
|
|
5080
|
+
rubric: zod_v4.strictObject({
|
|
5081
|
+
label: zod_v4.string().optional(),
|
|
5082
|
+
criteria: zod_v4.array(zod_v4.strictObject({
|
|
5083
|
+
...RedactedWrittenResponseRubricCriterionSchema.shape,
|
|
5084
|
+
measure: zod_v4.enum(SPEAKING_MEASURES).optional()
|
|
5085
|
+
})).min(1)
|
|
5086
|
+
}).optional(),
|
|
5087
|
+
languageTarget: zod_v4.string().optional()
|
|
5088
|
+
});
|
|
4815
5089
|
// src/scoring/activity-scorers/dictation.ts
|
|
4816
5090
|
/**
|
|
4817
5091
|
* Scores a Dictation response: the character-level similarity of what the
|
|
@@ -5416,6 +5690,34 @@ const READ_ALOUD_FIELD_POLICY = {
|
|
|
5416
5690
|
feedback: FEEDBACK_FIELD_POLICY
|
|
5417
5691
|
};
|
|
5418
5692
|
/**
|
|
5693
|
+
* Speaking Response: public, as written-response is. The task, the picture and
|
|
5694
|
+
* the limits of a take are what the learner needs to answer; the rubric tells
|
|
5695
|
+
* them what the answer is graded on — classified key by key, a criterion's
|
|
5696
|
+
* `measure` included, so nothing parked on it reaches a learner. A deployment
|
|
5697
|
+
* that wants the rubric hidden tightens it per call: the redacted schema leaves
|
|
5698
|
+
* it optional. Only the authored pass/fail feedback is removed.
|
|
5699
|
+
*/
|
|
5700
|
+
const SPEAKING_RESPONSE_FIELD_POLICY = {
|
|
5701
|
+
...SHARED_PUBLIC_FIELDS,
|
|
5702
|
+
instructions: "public",
|
|
5703
|
+
prompt: "public",
|
|
5704
|
+
promptHtml: "public",
|
|
5705
|
+
recording: {
|
|
5706
|
+
maxSeconds: "public",
|
|
5707
|
+
minSeconds: "public",
|
|
5708
|
+
maxTakes: "public"
|
|
5709
|
+
},
|
|
5710
|
+
languageTarget: "public",
|
|
5711
|
+
feedback: FEEDBACK_FIELD_POLICY,
|
|
5712
|
+
rubric: {
|
|
5713
|
+
...RUBRIC_FIELD_POLICY,
|
|
5714
|
+
criteria: {
|
|
5715
|
+
...RUBRIC_FIELD_POLICY.criteria,
|
|
5716
|
+
measure: "public"
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
};
|
|
5720
|
+
/**
|
|
5419
5721
|
* Whether a response carries a stored recording: an object whose `key` is a
|
|
5420
5722
|
* non-empty string. Read defensively, because both callers are handed whatever
|
|
5421
5723
|
* an application stored — `evaluate()` passes `partial` a response that may be
|
|
@@ -5470,7 +5772,32 @@ const BUILT_IN_DESCRIPTORS = {
|
|
|
5470
5772
|
"written-response": writtenResponseType,
|
|
5471
5773
|
"gap-select": gapSelectType,
|
|
5472
5774
|
dictation: dictationType,
|
|
5473
|
-
"read-aloud": readAloudType
|
|
5775
|
+
"read-aloud": readAloudType,
|
|
5776
|
+
"speaking-response": defineActivityType({
|
|
5777
|
+
type: "speaking-response",
|
|
5778
|
+
schema: SpeakingResponseDataSchema,
|
|
5779
|
+
scoring: {
|
|
5780
|
+
kind: "deferred",
|
|
5781
|
+
reason: "requires_async_grading",
|
|
5782
|
+
partial: (_data, response) => ({ hasRecording: hasRecording(response) })
|
|
5783
|
+
},
|
|
5784
|
+
isAnswered: (response) => hasRecording(response),
|
|
5785
|
+
fieldPolicy: SPEAKING_RESPONSE_FIELD_POLICY,
|
|
5786
|
+
redactedSchema: RedactedSpeakingResponseDataSchema,
|
|
5787
|
+
interop: {
|
|
5788
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
5789
|
+
xapiInteractionType: "other",
|
|
5790
|
+
correctResponsesPattern: () => []
|
|
5791
|
+
},
|
|
5792
|
+
interactions: [
|
|
5793
|
+
"recording-started",
|
|
5794
|
+
"recording-stopped",
|
|
5795
|
+
"recording-uploaded",
|
|
5796
|
+
"submitted"
|
|
5797
|
+
],
|
|
5798
|
+
authoring: speakingResponseAuthoring,
|
|
5799
|
+
mediaSlots: () => MAIN_MEDIA
|
|
5800
|
+
})
|
|
5474
5801
|
};
|
|
5475
5802
|
// src/schemas/read-schema.ts
|
|
5476
5803
|
/**
|
|
@@ -5748,6 +6075,12 @@ Object.defineProperty(exports, "MultipleChoiceOptionMediaSchema", {
|
|
|
5748
6075
|
return MultipleChoiceOptionMediaSchema;
|
|
5749
6076
|
}
|
|
5750
6077
|
});
|
|
6078
|
+
Object.defineProperty(exports, "PLACEHOLDER_RE", {
|
|
6079
|
+
enumerable: true,
|
|
6080
|
+
get: function() {
|
|
6081
|
+
return PLACEHOLDER_RE;
|
|
6082
|
+
}
|
|
6083
|
+
});
|
|
5751
6084
|
Object.defineProperty(exports, "READ_ALOUD_MAX_DIMENSION_WEIGHT", {
|
|
5752
6085
|
enumerable: true,
|
|
5753
6086
|
get: function() {
|
|
@@ -5796,6 +6129,12 @@ Object.defineProperty(exports, "SCRIPT_MAX_TURNS", {
|
|
|
5796
6129
|
return SCRIPT_MAX_TURNS;
|
|
5797
6130
|
}
|
|
5798
6131
|
});
|
|
6132
|
+
Object.defineProperty(exports, "SPEAKING_MEASURES", {
|
|
6133
|
+
enumerable: true,
|
|
6134
|
+
get: function() {
|
|
6135
|
+
return SPEAKING_MEASURES;
|
|
6136
|
+
}
|
|
6137
|
+
});
|
|
5799
6138
|
Object.defineProperty(exports, "SPEECH_ASSESSMENT_MAX_TEXT_LENGTH", {
|
|
5800
6139
|
enumerable: true,
|
|
5801
6140
|
get: function() {
|
|
@@ -5820,6 +6159,12 @@ Object.defineProperty(exports, "ScriptSchema", {
|
|
|
5820
6159
|
return ScriptSchema;
|
|
5821
6160
|
}
|
|
5822
6161
|
});
|
|
6162
|
+
Object.defineProperty(exports, "SpeakingResponseDataSchema", {
|
|
6163
|
+
enumerable: true,
|
|
6164
|
+
get: function() {
|
|
6165
|
+
return SpeakingResponseDataSchema;
|
|
6166
|
+
}
|
|
6167
|
+
});
|
|
5823
6168
|
Object.defineProperty(exports, "UnknownActivityTypeError", {
|
|
5824
6169
|
enumerable: true,
|
|
5825
6170
|
get: function() {
|
|
@@ -6054,6 +6399,12 @@ Object.defineProperty(exports, "numberedPassage", {
|
|
|
6054
6399
|
return numberedPassage;
|
|
6055
6400
|
}
|
|
6056
6401
|
});
|
|
6402
|
+
Object.defineProperty(exports, "passageWithoutPlaceholders", {
|
|
6403
|
+
enumerable: true,
|
|
6404
|
+
get: function() {
|
|
6405
|
+
return passageWithoutPlaceholders;
|
|
6406
|
+
}
|
|
6407
|
+
});
|
|
6057
6408
|
Object.defineProperty(exports, "pathKey", {
|
|
6058
6409
|
enumerable: true,
|
|
6059
6410
|
get: function() {
|
|
@@ -6066,6 +6417,12 @@ Object.defineProperty(exports, "placeholderOrder", {
|
|
|
6066
6417
|
return placeholderOrder;
|
|
6067
6418
|
}
|
|
6068
6419
|
});
|
|
6420
|
+
Object.defineProperty(exports, "policyFor", {
|
|
6421
|
+
enumerable: true,
|
|
6422
|
+
get: function() {
|
|
6423
|
+
return policyFor;
|
|
6424
|
+
}
|
|
6425
|
+
});
|
|
6069
6426
|
Object.defineProperty(exports, "readAloudType", {
|
|
6070
6427
|
enumerable: true,
|
|
6071
6428
|
get: function() {
|
|
@@ -6127,4 +6484,4 @@ Object.defineProperty(exports, "writtenResponseType", {
|
|
|
6127
6484
|
}
|
|
6128
6485
|
});
|
|
6129
6486
|
|
|
6130
|
-
//# sourceMappingURL=registry-
|
|
6487
|
+
//# sourceMappingURL=registry-C9O9txy5.cjs.map
|