@intellectif/lk-core 1.8.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/ai-check.cjs +2 -2
  3. package/dist/ai-check.d.cts +1 -1
  4. package/dist/ai-check.d.ts +1 -1
  5. package/dist/ai-check.js +2 -2
  6. package/dist/{ai-writing-DeIV-GfN.js → ai-writing-DQjI9EFN.js} +130 -5
  7. package/dist/ai-writing-DQjI9EFN.js.map +1 -0
  8. package/dist/{ai-writing-Bh2Y53cY.cjs → ai-writing-DbpZpGRk.cjs} +135 -4
  9. package/dist/ai-writing-DbpZpGRk.cjs.map +1 -0
  10. package/dist/{ai-Cnm0qE8v.d.cts → ai-zhc9yhRj.d.cts} +160 -2
  11. package/dist/ai-zhc9yhRj.d.cts.map +1 -0
  12. package/dist/{ai-Cnm0qE8v.d.ts → ai-zhc9yhRj.d.ts} +160 -2
  13. package/dist/ai-zhc9yhRj.d.ts.map +1 -0
  14. package/dist/{index-DMh3OYSb.d.cts → index-BeC8tAWf.d.cts} +2 -2
  15. package/dist/{index-DMh3OYSb.d.cts.map → index-BeC8tAWf.d.cts.map} +1 -1
  16. package/dist/{index-CtoPhJAH.d.ts → index-Byar5VVs.d.ts} +2 -2
  17. package/dist/{index-CtoPhJAH.d.ts.map → index-Byar5VVs.d.ts.map} +1 -1
  18. package/dist/{index-fZ67k-WR.d.cts → index-Ccj8oL_E.d.cts} +2 -2
  19. package/dist/{index-fZ67k-WR.d.cts.map → index-Ccj8oL_E.d.cts.map} +1 -1
  20. package/dist/{index-g-EWIugp.d.ts → index-CxkqS62X.d.ts} +89 -7
  21. package/dist/index-CxkqS62X.d.ts.map +1 -0
  22. package/dist/{index-oST8GBFa.d.cts → index-DLsUnGHt.d.cts} +89 -7
  23. package/dist/index-DLsUnGHt.d.cts.map +1 -0
  24. package/dist/{index-HXvg0YRQ.d.ts → index-f4YuMgFb.d.ts} +2 -2
  25. package/dist/{index-HXvg0YRQ.d.ts.map → index-f4YuMgFb.d.ts.map} +1 -1
  26. package/dist/index.cjs +8 -6
  27. package/dist/index.d.cts +40 -7
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts +40 -7
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +7 -7
  32. package/dist/{item-group-BceUoxoM.js → item-group-CKZk3txt.js} +2 -2
  33. package/dist/{item-group-BceUoxoM.js.map → item-group-CKZk3txt.js.map} +1 -1
  34. package/dist/{item-group-KV4hYu3a.cjs → item-group-COP5OwEC.cjs} +2 -2
  35. package/dist/{item-group-KV4hYu3a.cjs.map → item-group-COP5OwEC.cjs.map} +1 -1
  36. package/dist/{registry-tkacvJUI.js → registry-Bw9grsCN.js} +321 -11
  37. package/dist/registry-Bw9grsCN.js.map +1 -0
  38. package/dist/{registry-Cymq7Ah0.cjs → registry-C9O9txy5.cjs} +332 -10
  39. package/dist/registry-C9O9txy5.cjs.map +1 -0
  40. package/dist/{schemas-DM8sN_f0.js → schemas-BJe3W6Xb.js} +3 -3
  41. package/dist/{schemas-DM8sN_f0.js.map → schemas-BJe3W6Xb.js.map} +1 -1
  42. package/dist/{schemas-wDzbKdKa.cjs → schemas-CXlemRvB.cjs} +3 -3
  43. package/dist/{schemas-wDzbKdKa.cjs.map → schemas-CXlemRvB.cjs.map} +1 -1
  44. package/dist/schemas.cjs +3 -3
  45. package/dist/schemas.d.cts +1 -1
  46. package/dist/schemas.d.ts +1 -1
  47. package/dist/schemas.js +3 -3
  48. package/dist/{scoring-DIBmo_cy.js → scoring-BGKdff2i.js} +214 -26
  49. package/dist/scoring-BGKdff2i.js.map +1 -0
  50. package/dist/{scoring-D9tFvPEG.cjs → scoring-Db1CZeAn.cjs} +219 -25
  51. package/dist/scoring-Db1CZeAn.cjs.map +1 -0
  52. package/dist/scoring.cjs +3 -2
  53. package/dist/scoring.d.cts +3 -3
  54. package/dist/scoring.d.ts +3 -3
  55. package/dist/scoring.js +3 -3
  56. package/dist/{xapi-gCAo549Q.js → xapi-BtBm8AWZ.js} +2 -2
  57. package/dist/{xapi-gCAo549Q.js.map → xapi-BtBm8AWZ.js.map} +1 -1
  58. package/dist/{xapi-Dyqcl3JK.cjs → xapi-DOAqH9__.cjs} +2 -2
  59. package/dist/{xapi-Dyqcl3JK.cjs.map → xapi-DOAqH9__.cjs.map} +1 -1
  60. package/dist/xapi.cjs +1 -1
  61. package/dist/xapi.d.cts +1 -1
  62. package/dist/xapi.d.ts +1 -1
  63. package/dist/xapi.js +1 -1
  64. package/package.json +1 -1
  65. package/vectors/scoring.json +11967 -1
  66. package/vectors/validation.json +1298 -1
  67. package/dist/ai-Cnm0qE8v.d.cts.map +0 -1
  68. package/dist/ai-Cnm0qE8v.d.ts.map +0 -1
  69. package/dist/ai-writing-Bh2Y53cY.cjs.map +0 -1
  70. package/dist/ai-writing-DeIV-GfN.js.map +0 -1
  71. package/dist/index-g-EWIugp.d.ts.map +0 -1
  72. package/dist/index-oST8GBFa.d.cts.map +0 -1
  73. package/dist/registry-Cymq7Ah0.cjs.map +0 -1
  74. package/dist/registry-tkacvJUI.js.map +0 -1
  75. package/dist/scoring-D9tFvPEG.cjs.map +0 -1
  76. package/dist/scoring-DIBmo_cy.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$1(draft.media));
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$1(media) {
2665
+ function checkRecording$2(media) {
2648
2666
  if (!isRecord(media)) return [];
2649
2667
  const issues = [];
2650
2668
  const kind = media.type;
@@ -4172,8 +4190,8 @@ const readAloudAuthoring = {
4172
4190
  function checkReadAloudDraft(draft) {
4173
4191
  const issues = checkIdentity(draft, "read-aloud");
4174
4192
  issues.push(...checkReferenceText(draft.referenceText));
4175
- issues.push(...checkLocale(draft.locale));
4176
- issues.push(...checkRecording(draft.recording));
4193
+ issues.push(...checkLocale$1(draft.locale));
4194
+ issues.push(...checkRecording$1(draft.recording));
4177
4195
  issues.push(...checkDimensions(draft.scoring));
4178
4196
  issues.push(...checkModelRecording(draft.media));
4179
4197
  issues.push(...checkSlowRecording(draft));
@@ -4190,7 +4208,7 @@ function checkReferenceText(referenceText) {
4190
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."));
4191
4209
  return issues;
4192
4210
  }
4193
- function checkLocale(locale) {
4211
+ function checkLocale$1(locale) {
4194
4212
  if (isUnwritten(locale)) return [issue("ra_locale_required", ["locale"], "Choose the language the learner reads in, with its region — \"en-US\", not \"en\".")];
4195
4213
  if (typeof locale !== "string" || CANONICAL_LOCALE_RE.test(locale)) return [];
4196
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.")];
@@ -4200,7 +4218,7 @@ function checkLocale(locale) {
4200
4218
  * none of them decides a grade, but the longest take has to be set before a
4201
4219
  * recorder can be shown at all.
4202
4220
  */
4203
- function checkRecording(recording) {
4221
+ function checkRecording$1(recording) {
4204
4222
  if (isUnset(recording)) return [maxSecondsRequired()];
4205
4223
  if (!isRecord(recording)) return [];
4206
4224
  const issues = [];
@@ -4300,6 +4318,217 @@ function checkSlowRecording(draft) {
4300
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."));
4301
4319
  return issues;
4302
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
+ }
4303
4532
  // src/authoring/written-response.ts
4304
4533
  /**
4305
4534
  * Draft support for `written-response`.
@@ -4829,6 +5058,34 @@ const RedactedReadAloudDataSchema = zod_v4.strictObject({
4829
5058
  path: ["slowMedia", "url"]
4830
5059
  });
4831
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
+ });
4832
5089
  // src/scoring/activity-scorers/dictation.ts
4833
5090
  /**
4834
5091
  * Scores a Dictation response: the character-level similarity of what the
@@ -5433,6 +5690,34 @@ const READ_ALOUD_FIELD_POLICY = {
5433
5690
  feedback: FEEDBACK_FIELD_POLICY
5434
5691
  };
5435
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
+ /**
5436
5721
  * Whether a response carries a stored recording: an object whose `key` is a
5437
5722
  * non-empty string. Read defensively, because both callers are handed whatever
5438
5723
  * an application stored — `evaluate()` passes `partial` a response that may be
@@ -5487,7 +5772,32 @@ const BUILT_IN_DESCRIPTORS = {
5487
5772
  "written-response": writtenResponseType,
5488
5773
  "gap-select": gapSelectType,
5489
5774
  dictation: dictationType,
5490
- "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
+ })
5491
5801
  };
5492
5802
  // src/schemas/read-schema.ts
5493
5803
  /**
@@ -5819,6 +6129,12 @@ Object.defineProperty(exports, "SCRIPT_MAX_TURNS", {
5819
6129
  return SCRIPT_MAX_TURNS;
5820
6130
  }
5821
6131
  });
6132
+ Object.defineProperty(exports, "SPEAKING_MEASURES", {
6133
+ enumerable: true,
6134
+ get: function() {
6135
+ return SPEAKING_MEASURES;
6136
+ }
6137
+ });
5822
6138
  Object.defineProperty(exports, "SPEECH_ASSESSMENT_MAX_TEXT_LENGTH", {
5823
6139
  enumerable: true,
5824
6140
  get: function() {
@@ -5843,6 +6159,12 @@ Object.defineProperty(exports, "ScriptSchema", {
5843
6159
  return ScriptSchema;
5844
6160
  }
5845
6161
  });
6162
+ Object.defineProperty(exports, "SpeakingResponseDataSchema", {
6163
+ enumerable: true,
6164
+ get: function() {
6165
+ return SpeakingResponseDataSchema;
6166
+ }
6167
+ });
5846
6168
  Object.defineProperty(exports, "UnknownActivityTypeError", {
5847
6169
  enumerable: true,
5848
6170
  get: function() {
@@ -6162,4 +6484,4 @@ Object.defineProperty(exports, "writtenResponseType", {
6162
6484
  }
6163
6485
  });
6164
6486
 
6165
- //# sourceMappingURL=registry-Cymq7Ah0.cjs.map
6487
+ //# sourceMappingURL=registry-C9O9txy5.cjs.map