@intellectif/lk-core 1.4.0 → 1.6.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 (89) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +2 -0
  3. package/dist/ai-check.cjs +175 -17
  4. package/dist/ai-check.cjs.map +1 -1
  5. package/dist/ai-check.d.cts +13 -5
  6. package/dist/ai-check.d.cts.map +1 -1
  7. package/dist/ai-check.d.ts +13 -5
  8. package/dist/ai-check.d.ts.map +1 -1
  9. package/dist/ai-check.js +175 -17
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{ai-CnmLPMzd.d.ts → ai-i3jwXnpX.d.cts} +242 -7
  12. package/dist/ai-i3jwXnpX.d.cts.map +1 -0
  13. package/dist/{ai-CnmLPMzd.d.cts → ai-i3jwXnpX.d.ts} +242 -7
  14. package/dist/ai-i3jwXnpX.d.ts.map +1 -0
  15. package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-C4YRVrFi.cjs} +683 -72
  16. package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
  17. package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-OJ5m_1EB.js} +663 -88
  18. package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
  19. package/dist/{index-BqMXWuNC.d.cts → index--U7hHV0s.d.cts} +2 -2
  20. package/dist/{index-BqMXWuNC.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
  21. package/dist/{index-B54baFUA.d.ts → index-CFRgZLhH.d.cts} +45 -12
  22. package/dist/index-CFRgZLhH.d.cts.map +1 -0
  23. package/dist/{index-CVWvymVb.d.ts → index-DeD8EI1D.d.ts} +327 -3
  24. package/dist/index-DeD8EI1D.d.ts.map +1 -0
  25. package/dist/{index-CcHDTxIZ.d.ts → index-Dj92T76o.d.ts} +2 -2
  26. package/dist/{index-CcHDTxIZ.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
  27. package/dist/{index-BAfEHCnD.d.cts → index-Uh1u57yq.d.cts} +327 -3
  28. package/dist/index-Uh1u57yq.d.cts.map +1 -0
  29. package/dist/{index-2nigSf4N.d.cts → index-WBHSrIUU.d.ts} +45 -12
  30. package/dist/index-WBHSrIUU.d.ts.map +1 -0
  31. package/dist/index.cjs +88 -131
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +217 -95
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +217 -95
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +60 -118
  38. package/dist/index.js.map +1 -1
  39. package/dist/{item-group-DXiR8nOr.js → item-group-BbvvH_Ob.js} +3 -2
  40. package/dist/item-group-BbvvH_Ob.js.map +1 -0
  41. package/dist/{item-group-DroFXgLb.cjs → item-group-CQRbb8Rx.cjs} +3 -2
  42. package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
  43. package/dist/{registry-Dsx9mNsS.js → registry-B23o9UWh.js} +218 -21
  44. package/dist/registry-B23o9UWh.js.map +1 -0
  45. package/dist/{registry-DVi0LR_i.cjs → registry-Ih5n4PVZ.cjs} +265 -20
  46. package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
  47. package/dist/{schemas-DVbAOrtd.cjs → schemas-Q2dyOgc2.cjs} +77 -14
  48. package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
  49. package/dist/{schemas-Bdw0nGPo.js → schemas-yId-bJhD.js} +72 -15
  50. package/dist/schemas-yId-bJhD.js.map +1 -0
  51. package/dist/schemas.cjs +5 -2
  52. package/dist/schemas.d.cts +2 -2
  53. package/dist/schemas.d.ts +2 -2
  54. package/dist/schemas.js +4 -3
  55. package/dist/{scoring-BZqUkuDF.js → scoring-CLMdk8i9.js} +657 -3
  56. package/dist/scoring-CLMdk8i9.js.map +1 -0
  57. package/dist/{scoring-B2_HEhMT.cjs → scoring-CaPS6q8b.cjs} +722 -2
  58. package/dist/scoring-CaPS6q8b.cjs.map +1 -0
  59. package/dist/scoring.cjs +8 -2
  60. package/dist/scoring.d.cts +3 -3
  61. package/dist/scoring.d.ts +3 -3
  62. package/dist/scoring.js +3 -3
  63. package/dist/{xapi-DNzkSJ0S.cjs → xapi-BX0hvRRB.cjs} +2 -2
  64. package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
  65. package/dist/{xapi-CMTph7wG.js → xapi-Cdk9fTvV.js} +2 -2
  66. package/dist/{xapi-CMTph7wG.js.map → xapi-Cdk9fTvV.js.map} +1 -1
  67. package/dist/xapi.cjs +1 -1
  68. package/dist/xapi.d.cts +1 -1
  69. package/dist/xapi.d.ts +1 -1
  70. package/dist/xapi.js +1 -2
  71. package/package.json +2 -2
  72. package/vectors/README.md +24 -1
  73. package/vectors/scoring.json +18182 -1
  74. package/dist/ai-CnmLPMzd.d.cts.map +0 -1
  75. package/dist/ai-CnmLPMzd.d.ts.map +0 -1
  76. package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
  77. package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
  78. package/dist/index-2nigSf4N.d.cts.map +0 -1
  79. package/dist/index-B54baFUA.d.ts.map +0 -1
  80. package/dist/index-BAfEHCnD.d.cts.map +0 -1
  81. package/dist/index-CVWvymVb.d.ts.map +0 -1
  82. package/dist/item-group-DXiR8nOr.js.map +0 -1
  83. package/dist/item-group-DroFXgLb.cjs.map +0 -1
  84. package/dist/registry-DVi0LR_i.cjs.map +0 -1
  85. package/dist/registry-Dsx9mNsS.js.map +0 -1
  86. package/dist/schemas-Bdw0nGPo.js.map +0 -1
  87. package/dist/schemas-DVbAOrtd.cjs.map +0 -1
  88. package/dist/scoring-B2_HEhMT.cjs.map +0 -1
  89. package/dist/scoring-BZqUkuDF.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item-group-CQRbb8Rx.cjs","names":["z","MediaSchema","ScriptSchema","maxUnits","RedactedMediaSchema","groupCaptionsField","GROUP_CAPTIONS_REVEAL_DICTATION","getActivityTypeDescriptor","readSchema"],"sources":["../src/timeline-limits.ts","../src/schemas/item-group.ts"],"sourcesContent":["/**\n * The bounds of an interactive video — an item group with a `timeline`.\n *\n * A leaf module on purpose: the schemas, the authoring checks and the timeline\n * helpers all read these, and nothing here may import any of them back.\n */\n\n/**\n * The item types an interactive video may present. CLOSED on purpose: a type\n * added to the SDK later, or a custom type a host registers, is not silently\n * embeddable. Each one here was chosen, and each works inside a paused video —\n * a read-aloud's microphone included, because every quiz pauses the video\n * first.\n */\nexport const INTERACTIVE_VIDEO_ITEM_TYPES = [\n 'multiple-choice',\n 'fill-in-the-blanks',\n 'gap-select',\n 'dictation',\n 'read-aloud',\n] as const;\n\n/** One of {@link INTERACTIVE_VIDEO_ITEM_TYPES}. */\nexport type InteractiveVideoItemType = (typeof INTERACTIVE_VIDEO_ITEM_TYPES)[number];\n\n/** Whether `type` is one of the item types an interactive video may present. */\nexport function isInteractiveVideoItemType(type: unknown): type is InteractiveVideoItemType {\n return (INTERACTIVE_VIDEO_ITEM_TYPES as readonly unknown[]).includes(type);\n}\n\n/** The most quizzes one video may hold. */\nexport const TIMELINE_MAX_QUIZZES = 100;\n\n/**\n * The most questions one interactive video may hold, across all its quizzes.\n * The player keeps every one of them mounted, so an answer survives the\n * learner rewinding past its quiz.\n */\nexport const TIMELINE_MAX_ITEMS = 200;\n\n/** The most chapters one video may hold. */\nexport const TIMELINE_MAX_CHAPTERS = 100;\n\n/** The longest quiz or chapter title, in UTF-16 units. A title sits on one line of the player. */\nexport const TIMELINE_MAX_TITLE_LENGTH = 120;\n","import * as z from 'zod/v4';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport {\n INTERACTIVE_VIDEO_ITEM_TYPES,\n isInteractiveVideoItemType,\n TIMELINE_MAX_CHAPTERS,\n TIMELINE_MAX_ITEMS,\n TIMELINE_MAX_QUIZZES,\n TIMELINE_MAX_TITLE_LENGTH,\n} from '../timeline-limits.js';\nimport type { ActivityData, ValidationError, ValidationResult } from '../types/activity.js';\nimport type { ItemGroup, StimulusKind } from '../types/item-group.js';\nimport { GROUP_CAPTIONS_REVEAL_DICTATION, groupCaptionsField } from './dictation.js';\nimport { MediaSchema, RedactedMediaSchema, ScriptSchema } from './media.js';\nimport { readSchema } from './read-schema.js';\nimport { maxUnits } from './text-length.js';\n\n/**\n * An authored slot key.\n *\n * A `.` is rejected here, not at render time: `flattenSequence` uses the first\n * `.` to separate a group from its item, so a key containing one would produce\n * an ambiguous slot id. `keyOf` already threw on it — this moves the rejection\n * to `validateItemGroup`, where an author finds it, instead of leaving it to\n * surface when a learner opens the paper. Content that would newly fail\n * validation already threw at flatten time, so it can never have been sat.\n */\nconst SlotKeySchema = z\n .string()\n .min(1)\n .refine((key) => !key.includes('.'), {\n error:\n 'slotKey must not contain \".\" — that character separates a group from its item in a slot id, so a key containing one would make the slot ambiguous.',\n });\n\nfunction hasBody(stimulus: { body?: string | undefined }): boolean {\n return typeof stimulus.body === 'string' && stimulus.body.trim().length > 0;\n}\n\n/** Which media types can carry a stimulus of each kind. `text`/`mixed` accept any. */\nfunction mediaFits(kind: StimulusKind, mediaType: string | undefined): boolean {\n switch (kind) {\n case 'audio':\n return mediaType === 'audio';\n case 'video':\n return mediaType === 'video' || mediaType === 'embed';\n case 'image':\n return mediaType === 'image';\n default:\n return true;\n }\n}\n\n/**\n * Zod schema for a {@link Stimulus}. Loose: unknown keys preserved.\n *\n * The semantic guards exist because a stimulus that does not carry what its\n * `kind` promises is an authoring error that must not reach an exam: an\n * \"audio\" stimulus with no recording renders as a blank panel above six\n * listening questions. Unrepresentable in JSON Schema; dropped from\n * `stimulusJsonSchema` by design.\n */\nexport const StimulusSchema = z\n .looseObject({\n id: z.string().min(1),\n kind: z.enum(['text', 'audio', 'video', 'image', 'mixed']),\n title: z.string().optional(),\n body: z.string().optional(),\n bodyHtml: z.string().optional(),\n media: MediaSchema.optional(),\n transcript: z.string().optional(),\n script: ScriptSchema.optional(),\n locale: z.string().optional(),\n attribution: z.string().optional(),\n })\n .refine((stimulus) => stimulus.bodyHtml === undefined || hasBody(stimulus), {\n error:\n 'bodyHtml requires a plain-text body: it is the accessible fallback rendered when no sanitiser is supplied.',\n path: ['body'],\n })\n .refine(\n (stimulus) => (stimulus.kind !== 'text' && stimulus.kind !== 'mixed') || hasBody(stimulus),\n {\n error: 'A text or mixed stimulus needs a non-empty body.',\n path: ['body'],\n },\n )\n .refine((stimulus) => stimulus.kind === 'text' || stimulus.media !== undefined, {\n error: 'An audio, video, image or mixed stimulus needs media.',\n path: ['media'],\n })\n .refine(\n (stimulus) => stimulus.media === undefined || mediaFits(stimulus.kind, stimulus.media.type),\n {\n error:\n 'media.type does not fit the stimulus kind: audio needs audio; video needs video or embed; image needs image.',\n path: ['media', 'type'],\n },\n );\n\nconst TimelineTitleSchema = maxUnits(z.string(), TIMELINE_MAX_TITLE_LENGTH).refine(\n (title) => title.trim().length > 0,\n { error: 'A title must not be empty.' },\n);\n\n/** Seconds from the start of the video. zod rejects NaN and ±Infinity itself. */\nconst TimelineTimeSchema = z.number().min(0, { error: 'A time is 0 seconds or more.' });\n\n/**\n * A quiz. STRICT, like every part of a timeline: a misspelt `requried` would\n * otherwise be kept and the quiz silently optional on a paper that believed it\n * was required.\n */\nexport const TimelineCueSchema = z.strictObject({\n id: maxUnits(z.string().min(1), 64),\n at: TimelineTimeSchema,\n itemIds: z.array(z.string().min(1)).min(1).max(TIMELINE_MAX_ITEMS),\n title: TimelineTitleSchema.optional(),\n required: z.boolean().optional(),\n});\n\nexport const TimelineChapterSchema = z.strictObject({\n at: TimelineTimeSchema,\n title: TimelineTitleSchema,\n});\n\n/**\n * A timeline on its own: its quizzes, chapters and navigation. The rules that\n * need the group — every item in exactly one quiz, a video stimulus, the five\n * item types — are {@link timelineIssues}, which both group schemas run.\n */\nexport const MediaTimelineSchema = z\n .strictObject({\n cues: z.array(TimelineCueSchema).max(TIMELINE_MAX_QUIZZES),\n chapters: z.array(TimelineChapterSchema).max(TIMELINE_MAX_CHAPTERS).optional(),\n navigation: z.enum(['free', 'no-skip-ahead']).optional(),\n })\n .check((ctx) => {\n const seen = new Set<string>();\n ctx.value.cues.forEach((cue, index) => {\n if (seen.has(cue.id)) {\n ctx.issues.push({\n code: 'custom',\n input: cue.id,\n message: `Quiz id \"${cue.id}\" is used twice: events, drafts and the contents panel name a quiz by its id.`,\n path: ['cues', index, 'id'],\n });\n }\n seen.add(cue.id);\n });\n // Strictly increasing, not merely sorted: two chapters starting at one\n // moment leave the first with no length, and the scrubber no room to draw it.\n const chapters = ctx.value.chapters ?? [];\n chapters.forEach((chapter, index) => {\n const previous = chapters[index - 1];\n if (previous !== undefined && chapter.at <= previous.at) {\n ctx.issues.push({\n code: 'custom',\n input: chapter.at,\n message: 'Chapter times must be strictly increasing.',\n path: ['chapters', index, 'at'],\n });\n }\n });\n });\n\n/** A URL only this document can resolve, or one that inlines the bytes. */\nfunction isLocalOnlyUrl(url: unknown): boolean {\n return typeof url === 'string' && /^(data|blob):/i.test(url);\n}\n\n/**\n * The rules of an interactive video that span the timeline, the stimulus and\n * the items. Run by the content schema and, for everything but the two\n * authoring-only rules, by the strict redacted schema — a projection built by\n * hand never passed through the content schema.\n *\n * Items are read defensively: the redacted schema leaves them opaque, and each\n * is proven against its own type's schema elsewhere.\n */\nfunction timelineIssues(\n group: {\n stimulus: {\n kind: string;\n media?:\n | {\n type: string;\n url: string;\n poster?: string | undefined;\n tracks?: readonly { src: string }[] | undefined;\n }\n | undefined;\n };\n items: readonly unknown[];\n shuffle?: string | undefined;\n timeline?: { cues: readonly { itemIds: readonly string[] }[] } | undefined;\n },\n scope: 'content' | 'redacted',\n): { path: (string | number)[]; message: string; input: unknown }[] {\n const timeline = group.timeline;\n if (timeline === undefined) {\n return [];\n }\n const issues: { path: (string | number)[]; message: string; input: unknown }[] = [];\n const media = group.stimulus.media;\n\n // Reported at the field that is wrong, so an editor's own check at that path\n // can stand in for it without hiding anything else about the stimulus.\n if (group.stimulus.kind !== 'video') {\n issues.push({\n path: ['stimulus', 'kind'],\n input: group.stimulus.kind,\n message:\n 'An interactive video needs a video stimulus: quizzes open at moments of a video, and nothing else has them.',\n });\n } else if (media !== undefined && media.type !== 'video') {\n issues.push({\n path: ['stimulus', 'media', 'type'],\n input: media.type,\n message:\n 'An interactive video needs a video file. An embedded provider player cannot be paused at the moment a quiz opens.',\n });\n }\n if (group.shuffle === 'within-group') {\n issues.push({\n path: ['shuffle'],\n input: group.shuffle,\n message:\n 'An interactive video presents its questions in the order its quizzes open; shuffle: \"within-group\" cannot apply to it.',\n });\n }\n if (group.items.length > TIMELINE_MAX_ITEMS) {\n issues.push({\n path: ['items'],\n input: group.items.length,\n message: `An interactive video holds at most ${TIMELINE_MAX_ITEMS} questions.`,\n });\n }\n\n const itemIds = new Map<string, number>();\n group.items.forEach((item, index) => {\n const {\n id,\n type,\n media: itemMedia,\n } = (item ?? {}) as { id?: unknown; type?: unknown; media?: unknown };\n if (typeof id === 'string') {\n itemIds.set(id, index);\n }\n if (!isInteractiveVideoItemType(type)) {\n issues.push({\n path: ['items', index, 'type'],\n input: type,\n message: `An interactive video may hold only ${INTERACTIVE_VIDEO_ITEM_TYPES.join(', ')} questions.`,\n });\n }\n // The \"play the stimulus\" fallback a dictation has in a listening group\n // means nothing halfway through a video.\n if (scope === 'content' && type === 'dictation' && itemMedia === undefined) {\n issues.push({\n path: ['items', index, 'media'],\n input: itemMedia,\n message: 'A dictation inside an interactive video needs its own recording.',\n });\n }\n });\n\n const placed = new Set<string>();\n timeline.cues.forEach((cue, cueIndex) => {\n cue.itemIds.forEach((itemId, position) => {\n const path = ['timeline', 'cues', cueIndex, 'itemIds', position];\n if (!itemIds.has(itemId)) {\n issues.push({\n path,\n input: itemId,\n message: `No item in this group has the id \"${itemId}\".`,\n });\n } else if (placed.has(itemId)) {\n issues.push({\n path,\n input: itemId,\n message: `Item \"${itemId}\" is placed twice: every question belongs to exactly one quiz.`,\n });\n }\n placed.add(itemId);\n });\n });\n for (const [itemId, index] of itemIds) {\n if (!placed.has(itemId)) {\n issues.push({\n path: ['items', index],\n input: itemId,\n message: `Item \"${itemId}\" is in no quiz, so the video would never show it.`,\n });\n }\n }\n\n // A `data:` video swells the payload and every fingerprint over it; a stored\n // `blob:` URL means nothing in another document.\n if (scope === 'content' && media !== undefined) {\n const urls: [unknown, (string | number)[]][] = [\n [media.url, ['stimulus', 'media', 'url']],\n [media.poster, ['stimulus', 'media', 'poster']],\n ...(media.tracks ?? []).map((track, index): [unknown, (string | number)[]] => [\n track.src,\n ['stimulus', 'media', 'tracks', index, 'src'],\n ]),\n ];\n for (const [url, path] of urls) {\n if (isLocalOnlyUrl(url)) {\n issues.push({\n path,\n input: url,\n message:\n 'An interactive video is stored and served: its URLs cannot be data: or blob: URLs.',\n });\n }\n }\n }\n return issues;\n}\n\n/**\n * The structural minimum of an item as seen by the CONTAINER schema. Each\n * item's own contract is checked against its registered schema by\n * {@link validateItemGroup} — a zod schema cannot dispatch on a registry\n * that consumers extend at runtime.\n */\nconst ItemShapeSchema = z.looseObject({\n type: z.string().min(1),\n id: z.string().min(1),\n slotKey: SlotKeySchema.optional(),\n});\n\n/**\n * Zod schema for an {@link ItemGroup} CONTAINER. Loose: unknown keys\n * preserved. Validates the group's own fields and the stimulus in full, and\n * each item only structurally (`type` and `id`); use {@link validateItemGroup}\n * to validate the items against their registered schemas as well.\n */\nexport const ItemGroupSchema = z\n .looseObject({\n schemaVersion: z.literal('1.0'),\n type: z.literal('item-group'),\n id: z.string().min(1),\n title: z.string().optional(),\n slotKey: SlotKeySchema.optional(),\n stimulus: StimulusSchema,\n items: z.array(ItemShapeSchema).min(1),\n shuffle: z.enum(['none', 'within-group']).optional(),\n timeline: MediaTimelineSchema.optional(),\n })\n .refine((group) => group.items.every((item) => item.type !== 'item-group'), {\n error: 'Item groups do not nest: every item must be an activity.',\n path: ['items'],\n })\n .refine((group) => new Set(group.items.map((item) => item.id)).size === group.items.length, {\n error: 'Item ids must be unique within a group.',\n path: ['items'],\n })\n .check((ctx) => {\n for (const issue of timelineIssues(ctx.value, 'content')) {\n ctx.issues.push({ code: 'custom', ...issue });\n }\n });\n\n/** Strict learner-safe stimulus: everything but the author-only `transcript`. */\nexport const RedactedStimulusSchema = z.strictObject({\n id: z.string().min(1),\n kind: z.enum(['text', 'audio', 'video', 'image', 'mixed']),\n title: z.string().optional(),\n body: z.string().optional(),\n bodyHtml: z.string().optional(),\n media: RedactedMediaSchema.optional(),\n locale: z.string().optional(),\n attribution: z.string().optional(),\n});\n\n/**\n * Strict learner-safe item group. Items are left opaque here — each is proven\n * learner-safe by `assertRedacted` against its OWN type's redacted schema,\n * which is the only place that knowledge lives. The one exception is the one\n * rule that spans the container and an item: a captioned stimulus recording\n * played to a dictation is not learner-safe, because the captions are the\n * dictation's answer.\n */\nexport const RedactedItemGroupSchema = z\n .strictObject({\n redacted: z.literal(true),\n schemaVersion: z.literal('1.0'),\n type: z.literal('item-group'),\n id: z.string().min(1),\n title: z.string().optional(),\n slotKey: SlotKeySchema.optional(),\n stimulus: RedactedStimulusSchema,\n items: z.array(z.unknown()).min(1),\n shuffle: z.enum(['none', 'within-group']).optional(),\n timeline: MediaTimelineSchema.optional(),\n })\n .check((ctx) => {\n for (const issue of timelineIssues(ctx.value, 'redacted')) {\n ctx.issues.push({ code: 'custom', ...issue });\n }\n const captions = groupCaptionsField(ctx.value);\n if (captions !== undefined) {\n ctx.issues.push({\n code: 'custom',\n input: ctx.value.stimulus.media?.[captions],\n message: GROUP_CAPTIONS_REVEAL_DICTATION,\n path: ['stimulus', 'media', captions],\n });\n }\n });\n\nfunction toValidationErrors(\n issues: readonly { path: readonly PropertyKey[]; message: string; code: string }[],\n prefix: string[],\n): ValidationError[] {\n return issues.map((issue) => ({\n path: [...prefix, ...issue.path.map(String)],\n message: issue.message,\n code: issue.code,\n }));\n}\n\n/**\n * Validates an item group in full: the container and stimulus against\n * {@link ItemGroupSchema}, then every item against the schema registered for\n * its `type`. Errors from items are reported at `items.<index>.…`.\n *\n * Unlike `validateActivity`, an item whose type is not registered is REPORTED\n * (code `unknown_activity_type`) rather than thrown: a group is validated as\n * a whole, and an author fixing a six-item group wants every problem listed,\n * not the first one that happened to throw.\n */\nexport function validateItemGroup(data: unknown): ValidationResult<ItemGroup> {\n const container = ItemGroupSchema.safeParse(data);\n if (!container.success) {\n return { success: false, errors: toValidationErrors(container.error.issues, []) };\n }\n\n const errors: ValidationError[] = [];\n const items: ActivityData[] = [];\n container.data.items.forEach((item, index) => {\n const descriptor = getActivityTypeDescriptor(item.type);\n if (descriptor === undefined) {\n errors.push({\n path: ['items', String(index), 'type'],\n message: `Activity type \"${item.type}\" is not registered`,\n code: 'unknown_activity_type',\n });\n return;\n }\n const parsed = readSchema(descriptor.schema, item, `Activity type \"${descriptor.type}\"`);\n if (!parsed.success) {\n errors.push(...toValidationErrors(parsed.issues, ['items', String(index)]));\n return;\n }\n items.push(parsed.data as ActivityData);\n });\n const captions = groupCaptionsField(container.data);\n if (captions !== undefined) {\n errors.push({\n path: ['stimulus', 'media', captions],\n message: GROUP_CAPTIONS_REVEAL_DICTATION,\n code: 'custom',\n });\n }\n\n if (errors.length > 0) {\n return { success: false, errors };\n }\n // zod4 optional outputs are `T | undefined`; the wire type uses exact\n // optionals. Structurally identical at runtime — the cast is type-level.\n return { success: true, data: { ...container.data, items } as unknown as ItemGroup };\n}\n"],"mappings":"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"}
@@ -134,6 +134,20 @@ const MediaTrackSchema = z.strictObject({
134
134
  default: z.boolean().optional()
135
135
  });
136
136
  const MAX_TRACKS = 12;
137
+ /** The most turns a recording's `script` may hold. */
138
+ const SCRIPT_MAX_TURNS = 500;
139
+ /**
140
+ * One speaker's turn in an author-only `script`. Loose, as content is: a host
141
+ * may keep its own fields on a turn (the voice it cast, a direction for it).
142
+ * Unlike a track's, both fields are required, so a misspelt one is still
143
+ * caught.
144
+ */
145
+ const ScriptTurnSchema = z.looseObject({
146
+ speaker: maxUnits(z.string(), 60).refine((speaker) => speaker.trim().length > 0, { error: "A script turn needs a speaker." }),
147
+ text: z.string().refine((text) => text.trim().length > 0, { error: "A script turn needs words." })
148
+ });
149
+ /** An author-only `script`: one turn or more, in the order they are spoken. */
150
+ const ScriptSchema = z.array(ScriptTurnSchema).min(1).max(500);
137
151
  /** `poster` on anything but a video: an image with nothing to stand in for. */
138
152
  function posterFits(m) {
139
153
  return m.poster === void 0 || m.type === "video";
@@ -176,7 +190,9 @@ const MediaSchema = z.looseObject({
176
190
  captionsUrl: MediaUrlSchema.optional(),
177
191
  tracks: z.array(MediaTrackSchema).min(1).max(MAX_TRACKS).optional(),
178
192
  poster: MediaUrlSchema.optional(),
179
- playback: MediaPlaybackSchema.optional()
193
+ playback: MediaPlaybackSchema.optional(),
194
+ transcript: z.string().optional(),
195
+ script: ScriptSchema.optional()
180
196
  }).refine((m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0, {
181
197
  error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
182
198
  path: ["alt"]
@@ -2732,6 +2748,97 @@ function countWords(text) {
2732
2748
  if (trimmed === "") return 0;
2733
2749
  return trimmed.split(/\s+/).length;
2734
2750
  }
2751
+ /** A label as labels are compared: composed, trimmed, its spacing collapsed, lower-cased. */
2752
+ function levelKey(label) {
2753
+ return label.normalize("NFC").trim().replace(/\s+/gu, " ").toLowerCase();
2754
+ }
2755
+ /**
2756
+ * The levels in `value`, or the first reason they cannot be used. `null` or
2757
+ * absent means the criterion has no levels, which is not a problem: it is
2758
+ * judged on a score.
2759
+ *
2760
+ * Each element and each field is read once, so a getter cannot pass the check
2761
+ * with one value and hand the grader another. The levels come back as written.
2762
+ */
2763
+ function readRubricLevels(value) {
2764
+ if (value === void 0 || value === null) return {
2765
+ ok: true,
2766
+ levels: null
2767
+ };
2768
+ if (!Array.isArray(value)) return {
2769
+ ok: false,
2770
+ problem: "not-a-list"
2771
+ };
2772
+ const count = value.length;
2773
+ if (count < 2) return {
2774
+ ok: false,
2775
+ problem: "too-few"
2776
+ };
2777
+ if (count > 10) return {
2778
+ ok: false,
2779
+ problem: "too-many"
2780
+ };
2781
+ const levels = [];
2782
+ const seen = /* @__PURE__ */ new Set();
2783
+ for (let index = 0; index < count; index += 1) {
2784
+ const entry = value[index];
2785
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return {
2786
+ ok: false,
2787
+ problem: "not-a-level",
2788
+ index
2789
+ };
2790
+ const record = entry;
2791
+ const label = record.label;
2792
+ const description = record.description;
2793
+ if (typeof label !== "string" || label.trim() === "") return {
2794
+ ok: false,
2795
+ problem: "no-label",
2796
+ index
2797
+ };
2798
+ if (description !== void 0 && typeof description !== "string") return {
2799
+ ok: false,
2800
+ problem: "bad-description",
2801
+ index
2802
+ };
2803
+ const key = levelKey(label);
2804
+ if (seen.has(key)) return {
2805
+ ok: false,
2806
+ problem: "duplicate-label",
2807
+ index
2808
+ };
2809
+ seen.add(key);
2810
+ levels.push({
2811
+ label,
2812
+ ...description !== void 0 ? { description } : {}
2813
+ });
2814
+ }
2815
+ return {
2816
+ ok: true,
2817
+ levels
2818
+ };
2819
+ }
2820
+ /**
2821
+ * How far level probabilities may sum from 1 and still be a distribution:
2822
+ * rounding in a grader's output, not a different judgement.
2823
+ */
2824
+ const LEVEL_PROBABILITY_TOLERANCE = 1e-6;
2825
+ /**
2826
+ * `values` as a distribution over `count` levels whose likeliest is level
2827
+ * `chosen`, a tie for likeliest included: each a finite number from 0 to 1,
2828
+ * together 1 within {@link LEVEL_PROBABILITY_TOLERANCE}. `null` when they are
2829
+ * not one.
2830
+ *
2831
+ * The AI check refuses a reply whose probabilities are not a distribution, and
2832
+ * calibration and routing read a grade's the same way, so probabilities the
2833
+ * check let through are never ignored downstream. Each element is read once,
2834
+ * a hole as `undefined`, into the copy returned.
2835
+ */
2836
+ function levelDistribution(values, count, chosen) {
2837
+ const read = Array.from(values);
2838
+ if (read.length !== count || !read.every((p) => typeof p === "number" && p >= 0 && p <= 1)) return null;
2839
+ const sum = read.reduce((total, p) => total + p, 0);
2840
+ return Math.abs(sum - 1) <= 1e-6 && read[chosen] === Math.max(...read) ? read : null;
2841
+ }
2735
2842
  //#endregion
2736
2843
  //#region src/scoring/strategies/all-or-nothing.ts
2737
2844
  /**
@@ -2948,7 +3055,11 @@ const ITEM_FINDING_SEVERITY = {
2948
3055
  gs_bank_no_distractor: "warning",
2949
3056
  gs_answer_in_passage: "warning",
2950
3057
  wr_criterion_name_duplicate: "warning",
3058
+ wr_levels_unreadable: "warning",
2951
3059
  ra_text_long_for_time: "warning",
3060
+ media_transcript_unused: "advice",
3061
+ media_script_differs: "warning",
3062
+ media_transcript_differs: "warning",
2952
3063
  set_key_position_same: "warning"
2953
3064
  };
2954
3065
  function finding(code, path, message) {
@@ -3198,14 +3309,38 @@ function critiqueGapSelect(draft) {
3198
3309
  });
3199
3310
  return findings;
3200
3311
  }
3312
+ /** Why a criterion's levels cannot be graded on, said to the author. */
3313
+ const LEVELS_PROBLEM = {
3314
+ "not-a-list": (criterion) => `Criterion ${criterion}'s levels are not a list`,
3315
+ "too-few": (criterion) => `Criterion ${criterion} has fewer than 2 levels`,
3316
+ "too-many": (criterion) => `Criterion ${criterion} has more than 10 levels`,
3317
+ "not-a-level": (criterion, level) => `Level ${level} of criterion ${criterion} is not a level`,
3318
+ "no-label": (criterion, level) => `Level ${level} of criterion ${criterion} has no label`,
3319
+ "bad-description": (criterion, level) => `Level ${level} of criterion ${criterion} has a description that is not text`,
3320
+ "duplicate-label": (criterion, level) => `Level ${level} of criterion ${criterion} has the label of an earlier level`
3321
+ };
3201
3322
  function critiqueWrittenResponse(draft) {
3202
3323
  const rubric = isRecord(draft.rubric) ? draft.rubric : void 0;
3203
- return duplicates(rubric === void 0 ? [] : listOf(rubric.criteria), (criterion) => isRecord(criterion) ? written(criterion.name) : void 0).map(({ first, second }) => finding("wr_criterion_name_duplicate", [
3324
+ const criteria = rubric === void 0 ? [] : listOf(rubric.criteria);
3325
+ const names = duplicates(criteria, (criterion) => isRecord(criterion) ? written(criterion.name) : void 0).map(({ first, second }) => finding("wr_criterion_name_duplicate", [
3204
3326
  "rubric",
3205
3327
  "criteria",
3206
3328
  second,
3207
3329
  "name"
3208
3330
  ], `Criteria ${ordinal(first)} and ${ordinal(second)} have the same name. Feedback and grades name a criterion, so one of them cannot be told apart.`));
3331
+ const levels = criteria.flatMap((criterion, index) => {
3332
+ if (!isRecord(criterion)) return [];
3333
+ const read = readRubricLevels(criterion.levels);
3334
+ if (read.ok) return [];
3335
+ const level = ordinal(read.index ?? 0);
3336
+ return [finding("wr_levels_unreadable", [
3337
+ "rubric",
3338
+ "criteria",
3339
+ index,
3340
+ "levels"
3341
+ ], `${LEVELS_PROBLEM[read.problem](ordinal(index), level)}, so a grader cannot judge against them.`)];
3342
+ });
3343
+ return [...names, ...levels];
3209
3344
  }
3210
3345
  /** 150 words a minute: a fluent adult reading aloud. A learner is slower. */
3211
3346
  const WORDS_PER_SECOND = 2.5;
@@ -3593,7 +3728,9 @@ const MultipleChoiceOptionMediaSchema = z.looseObject({
3593
3728
  type: z.enum(["image", "audio"]),
3594
3729
  url: MediaUrlSchema,
3595
3730
  alt: z.string().min(1).optional(),
3596
- captionsUrl: MediaUrlSchema.optional()
3731
+ captionsUrl: MediaUrlSchema.optional(),
3732
+ transcript: z.string().optional(),
3733
+ script: ScriptSchema.optional()
3597
3734
  }).refine((media) => media.type !== "image" || media.alt !== void 0 && media.alt !== "", {
3598
3735
  error: "An image option requires a non-empty `alt`.",
3599
3736
  path: ["alt"]
@@ -4387,7 +4524,11 @@ const WrittenResponseRubricSchema = z.looseObject({
4387
4524
  const RedactedWrittenResponseRubricCriterionSchema = z.strictObject({
4388
4525
  name: z.string().min(1),
4389
4526
  description: z.string().optional(),
4390
- weight: z.number().min(0)
4527
+ weight: z.number().min(0),
4528
+ levels: z.array(z.strictObject({
4529
+ label: z.string().min(1),
4530
+ description: z.string().optional()
4531
+ })).min(2).max(10).optional()
4391
4532
  });
4392
4533
  const RedactedWrittenResponseRubricSchema = z.strictObject({
4393
4534
  label: z.string().optional(),
@@ -4829,8 +4970,49 @@ function defineActivityType(descriptor) {
4829
4970
  return descriptor;
4830
4971
  }
4831
4972
  //#endregion
4973
+ //#region src/registry/keep-when.ts
4974
+ /**
4975
+ * A nested field policy that keeps its field only when the field's value
4976
+ * passes a test. Internal: the symbol is never exported, so no host policy can
4977
+ * carry one, and `FieldPolicy`'s string keys never see it.
4978
+ *
4979
+ * It can only drop a field the policy would otherwise emit, never emit one it
4980
+ * would drop, so redaction stays fail-closed. It exists for a field
4981
+ * `validateActivity` does not check in 1.x — a rubric criterion's `levels` —
4982
+ * whose malformed values would otherwise reach the strict redacted schema and
4983
+ * make `redact()` throw on content it accepted before.
4984
+ */
4985
+ const KEEP_WHEN = Symbol("lk.keepWhen");
4986
+ /** `policy`, kept only for values `test` accepts. */
4987
+ function keepWhen(policy, test) {
4988
+ const guarded = {
4989
+ ...policy,
4990
+ [KEEP_WHEN]: test
4991
+ };
4992
+ return Object.freeze(guarded);
4993
+ }
4994
+ /** The test a nested policy keeps its field by, when it has one. */
4995
+ function guardOf(policy) {
4996
+ return policy[KEEP_WHEN];
4997
+ }
4998
+ //#endregion
4832
4999
  //#region src/registry/builtins.ts
4833
5000
  /**
5001
+ * Where the built-in types keep their files (`mediaSlots`). Every type has its
5002
+ * `media`; dictation and read-aloud add a slower recording; a multiple-choice
5003
+ * option may carry a picture or a recording of its own.
5004
+ */
5005
+ const MAIN_MEDIA = [{ path: ["media"] }];
5006
+ const MAIN_AND_SLOW_MEDIA = [{ path: ["media"] }, { path: ["slowMedia"] }];
5007
+ function multipleChoiceMediaSlots(data) {
5008
+ const options = Array.isArray(data.options) ? data.options : [];
5009
+ return [...MAIN_MEDIA, ...options.map((_, index) => ({ path: [
5010
+ "options",
5011
+ index,
5012
+ "media"
5013
+ ] }))];
5014
+ }
5015
+ /**
4834
5016
  * Field-sensitivity policies for the built-in types (R7). Fail-closed:
4835
5017
  * anything not listed here is dropped by `redact()`. `scoringStrategy` is
4836
5018
  * answer-key everywhere — MC `partial` penalises wrong selections while
@@ -4850,10 +5032,11 @@ function defineActivityType(descriptor) {
4850
5032
  * documents ("a field the policy does not classify is removed") stopped at the
4851
5033
  * media boundary. It no longer does.
4852
5034
  *
4853
- * Everything here is public by necessity: the client is the thing that renders
4854
- * and enforces the policy, and "1 play remaining" is text the learner has to
4855
- * read. None of it is an answer key — a listening paper's answer key is
4856
- * `stimulus.transcript`, which stays `author-only`.
5035
+ * Public by necessity, except what the recording says: the client is the thing
5036
+ * that renders and enforces the policy, and "1 play remaining" is text the
5037
+ * learner has to read. `transcript` and `script` are the exception, and
5038
+ * `author-only`: what a listening question's recording says is that question's
5039
+ * answer, as `stimulus.transcript` is a listening paper's.
4857
5040
  */
4858
5041
  const MEDIA_FIELD_POLICY = {
4859
5042
  type: "public",
@@ -4874,7 +5057,9 @@ const MEDIA_FIELD_POLICY = {
4874
5057
  seek: "public",
4875
5058
  rate: "public",
4876
5059
  nativeControlHints: "public"
4877
- }
5060
+ },
5061
+ transcript: "author-only",
5062
+ script: "author-only"
4878
5063
  };
4879
5064
  const SHARED_PUBLIC_FIELDS = {
4880
5065
  schemaVersion: "public",
@@ -4944,7 +5129,9 @@ const MULTIPLE_CHOICE_FIELD_POLICY = {
4944
5129
  type: "public",
4945
5130
  url: "public",
4946
5131
  alt: "public",
4947
- captionsUrl: "public"
5132
+ captionsUrl: "public",
5133
+ transcript: "author-only",
5134
+ script: "author-only"
4948
5135
  }
4949
5136
  }
4950
5137
  };
@@ -5046,7 +5233,11 @@ const RUBRIC_FIELD_POLICY = {
5046
5233
  criteria: {
5047
5234
  name: "public",
5048
5235
  description: "public",
5049
- weight: "public"
5236
+ weight: "public",
5237
+ levels: keepWhen({
5238
+ label: "public",
5239
+ description: "public"
5240
+ }, (value) => readRubricLevels(value).ok)
5050
5241
  }
5051
5242
  };
5052
5243
  const WRITTEN_RESPONSE_FIELD_POLICY = {
@@ -5080,7 +5271,8 @@ const multipleChoiceType = defineActivityType({
5080
5271
  "option-deselected",
5081
5272
  "submitted"
5082
5273
  ],
5083
- authoring: multipleChoiceAuthoring
5274
+ authoring: multipleChoiceAuthoring,
5275
+ mediaSlots: multipleChoiceMediaSlots
5084
5276
  });
5085
5277
  /** Built-in Fill-in-the-Blanks descriptor. */
5086
5278
  const fillInTheBlanksType = defineActivityType({
@@ -5103,7 +5295,8 @@ const fillInTheBlanksType = defineActivityType({
5103
5295
  "hint-requested",
5104
5296
  "submitted"
5105
5297
  ],
5106
- authoring: fillInTheBlanksAuthoring
5298
+ authoring: fillInTheBlanksAuthoring,
5299
+ mediaSlots: () => MAIN_MEDIA
5107
5300
  });
5108
5301
  /**
5109
5302
  * Built-in Written Response descriptor. Grading is DEFERRED: submissions are
@@ -5135,7 +5328,8 @@ const writtenResponseType = defineActivityType({
5135
5328
  correctResponsesPattern: () => []
5136
5329
  },
5137
5330
  interactions: ["text-changed", "submitted"],
5138
- authoring: writtenResponseAuthoring
5331
+ authoring: writtenResponseAuthoring,
5332
+ mediaSlots: () => MAIN_MEDIA
5139
5333
  });
5140
5334
  /** Built-in Gap Select descriptor. */
5141
5335
  const gapSelectType = defineActivityType({
@@ -5154,7 +5348,8 @@ const gapSelectType = defineActivityType({
5154
5348
  correctResponsesPattern: (data) => [data.gaps.map((gap) => `${gap.id}[.]${gap.correctChoiceId}`).join("[,]")]
5155
5349
  },
5156
5350
  interactions: ["gap-selected", "submitted"],
5157
- authoring: gapSelectAuthoring
5351
+ authoring: gapSelectAuthoring,
5352
+ mediaSlots: () => MAIN_MEDIA
5158
5353
  });
5159
5354
  /**
5160
5355
  * Built-in Dictation descriptor. Graded synchronously: the score is the
@@ -5181,7 +5376,8 @@ const dictationType = defineActivityType({
5181
5376
  "hint-requested",
5182
5377
  "submitted"
5183
5378
  ],
5184
- authoring: dictationAuthoring
5379
+ authoring: dictationAuthoring,
5380
+ mediaSlots: () => MAIN_AND_SLOW_MEDIA
5185
5381
  });
5186
5382
  /**
5187
5383
  * Read Aloud, the type with no answer key at all.
@@ -5262,7 +5458,8 @@ const readAloudType = defineActivityType({
5262
5458
  "recording-uploaded",
5263
5459
  "submitted"
5264
5460
  ],
5265
- authoring: readAloudAuthoring
5461
+ authoring: readAloudAuthoring,
5462
+ mediaSlots: () => MAIN_AND_SLOW_MEDIA
5266
5463
  });
5267
5464
  /**
5268
5465
  * The SDK's own types, by type: what the registry is created holding
@@ -5390,10 +5587,10 @@ function isStandardSchema(value) {
5390
5587
  * A schema's own JSON Schema, when it implements the Standard JSON Schema
5391
5588
  * converter (`'~standard'.jsonSchema`) — zod 4.6 does — or `undefined`.
5392
5589
  */
5393
- function standardJsonSchemaOf(schema) {
5590
+ function standardJsonSchemaOf(schema, target = "draft-07") {
5394
5591
  const converter = schema["~standard"].jsonSchema;
5395
5592
  if (typeof converter?.output !== "function") return;
5396
- const generated = converter.output({ target: "draft-07" });
5593
+ const generated = converter.output({ target });
5397
5594
  return generated !== null && typeof generated === "object" && !Array.isArray(generated) ? generated : void 0;
5398
5595
  }
5399
5596
  //#endregion
@@ -5444,6 +5641,6 @@ function registeredActivityTypes() {
5444
5641
  return [...registry.keys()];
5445
5642
  }
5446
5643
  //#endregion
5447
- export { dictationReferenceWords as $, matchText as A, issue as B, SPEECH_ASSESSMENT_MAX_TEXT_LENGTH as C, keyPositionFinding as D, MultipleChoiceOptionMediaSchema as E, covers as F, groupCaptionsField as G, refusesEmpty as H, isMissingId as I, RedactedScoringError as J, ActivitySchemaError as K, isRecord as L, DRAFT_ISSUE_SEVERITY as M, checkMedia as N, normaliseFinding as O, coveredPathsOf as P, alignSequences as Q, isUnset as R, READ_ALOUD_MAX_TAKES as S, MultipleChoiceDataSchema as T, DictationDataSchema as U, pathKey as V, GROUP_CAPTIONS_REVEAL_DICTATION as W, STEP as X, UnknownActivityTypeError as Y, alignDictation as Z, ReadAloudDataSchema as _, MediaSchema as _t, readSchema as a, codePointLength as at, READ_ALOUD_MAX_REFERENCE_LENGTH as b, dictationType as c, BUILT_IN_ACTIVITY_TYPES as ct, multipleChoiceType as d, containsWords as dt, DICTATION_MAX_ACCEPTED_TRANSCRIPTS as et, readAloudType as f, fold as ft, GapSelectDataSchema as g, FillInTheBlanksDataSchema as gt, WrittenResponseDataSchema as h, revealsAnswer as ht, isOwnSchema as i, DICTATION_MAX_TRANSCRIPT_LENGTH as it, countWords as j, levenshteinDistance as k, fillInTheBlanksType as l, everyBuiltInTypeHandled as lt, defineActivityType as m, placeholderOrder as mt, registerActivityType as n, DICTATION_MAX_EQUIVALENCE_LENGTH as nt, standardJsonSchemaOf as o, normalizeDictationText as ot, writtenResponseType as p, numberedPassage as pt, DeferredScoringError as q, registeredActivityTypes as r, DICTATION_MAX_TEXT_LENGTH as rt, MEDIA_FIELD_POLICY as s, withValidationScope as st, getActivityTypeDescriptor as t, DICTATION_MAX_EQUIVALENCES as tt, gapSelectType as u, isBuiltInActivityType as ut, CANONICAL_LOCALE_RE as v, RedactedMediaSchema as vt, SPEECH_ASSESSMENT_MAX_WORDS as w, READ_ALOUD_MAX_SECONDS as x, READ_ALOUD_MAX_DIMENSION_WEIGHT as y, maxUnits as yt, isUnwritten as z };
5644
+ export { DeferredScoringError as $, normaliseFinding as A, coveredPathsOf as B, READ_ALOUD_MAX_TAKES as C, MediaSchema as Ct, MultipleChoiceOptionMediaSchema as D, maxUnits as Dt, MultipleChoiceDataSchema as E, ScriptSchema as Et, levelKey as F, isUnwritten as G, isMissingId as H, readRubricLevels as I, refusesEmpty as J, issue as K, countWords as L, matchText as M, LEVEL_PROBABILITY_TOLERANCE as N, keyPositionFinding as O, levelDistribution as P, ActivitySchemaError as Q, DRAFT_ISSUE_SEVERITY as R, READ_ALOUD_MAX_SECONDS as S, FillInTheBlanksDataSchema as St, SPEECH_ASSESSMENT_MAX_WORDS as T, SCRIPT_MAX_TURNS as Tt, isRecord as U, covers as V, isUnset as W, GROUP_CAPTIONS_REVEAL_DICTATION as X, DictationDataSchema as Y, groupCaptionsField as Z, GapSelectDataSchema as _, containsWords as _t, readSchema as a, dictationReferenceWords as at, READ_ALOUD_MAX_DIMENSION_WEIGHT as b, placeholderOrder as bt, dictationType as c, DICTATION_MAX_EQUIVALENCE_LENGTH as ct, multipleChoiceType as d, codePointLength as dt, RedactedScoringError as et, readAloudType as f, normalizeDictationText as ft, WrittenResponseDataSchema as g, isBuiltInActivityType as gt, defineActivityType as h, everyBuiltInTypeHandled as ht, isOwnSchema as i, alignSequences as it, levenshteinDistance as j, finding as k, fillInTheBlanksType as l, DICTATION_MAX_TEXT_LENGTH as lt, guardOf as m, BUILT_IN_ACTIVITY_TYPES as mt, registerActivityType as n, STEP as nt, standardJsonSchemaOf as o, DICTATION_MAX_ACCEPTED_TRANSCRIPTS as ot, writtenResponseType as p, withValidationScope as pt, pathKey as q, registeredActivityTypes as r, alignDictation as rt, MEDIA_FIELD_POLICY as s, DICTATION_MAX_EQUIVALENCES as st, getActivityTypeDescriptor as t, UnknownActivityTypeError as tt, gapSelectType as u, DICTATION_MAX_TRANSCRIPT_LENGTH as ut, ReadAloudDataSchema as v, fold as vt, SPEECH_ASSESSMENT_MAX_TEXT_LENGTH as w, RedactedMediaSchema as wt, READ_ALOUD_MAX_REFERENCE_LENGTH as x, revealsAnswer as xt, CANONICAL_LOCALE_RE as y, numberedPassage as yt, checkMedia as z };
5448
5645
 
5449
- //# sourceMappingURL=registry-Dsx9mNsS.js.map
5646
+ //# sourceMappingURL=registry-B23o9UWh.js.map