@intellectif/lk-core 1.3.0 → 1.5.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 (85) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +2 -0
  3. package/dist/{ai-B222dtHH.d.ts → ai-BYryFkF9.d.cts} +462 -5
  4. package/dist/ai-BYryFkF9.d.cts.map +1 -0
  5. package/dist/{ai-B222dtHH.d.cts → ai-BYryFkF9.d.ts} +462 -5
  6. package/dist/ai-BYryFkF9.d.ts.map +1 -0
  7. package/dist/ai-check.cjs +274 -23
  8. package/dist/ai-check.cjs.map +1 -1
  9. package/dist/ai-check.d.cts +13 -5
  10. package/dist/ai-check.d.cts.map +1 -1
  11. package/dist/ai-check.d.ts +13 -5
  12. package/dist/ai-check.d.ts.map +1 -1
  13. package/dist/ai-check.js +274 -23
  14. package/dist/ai-check.js.map +1 -1
  15. package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-Cen7-3qs.cjs} +520 -58
  16. package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
  17. package/dist/{ai-writing-CCBjTOM3.js → ai-writing-D8PdhJJz.js} +513 -75
  18. package/dist/ai-writing-D8PdhJJz.js.map +1 -0
  19. package/dist/{index-Dyu66R4-.d.cts → index-Bg0aeWtM.d.cts} +327 -3
  20. package/dist/index-Bg0aeWtM.d.cts.map +1 -0
  21. package/dist/{index-BWPRPIXL.d.ts → index-CxpiWedW.d.ts} +7 -217
  22. package/dist/index-CxpiWedW.d.ts.map +1 -0
  23. package/dist/{index-CXyOV8sm.d.cts → index-D7R3Ll8R.d.cts} +2 -2
  24. package/dist/{index-CXyOV8sm.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
  25. package/dist/{index-UGxRSopE.d.ts → index-DUaW1Tj5.d.ts} +327 -3
  26. package/dist/index-DUaW1Tj5.d.ts.map +1 -0
  27. package/dist/{index-O4_ExlAD.d.cts → index-qMbxed1B.d.cts} +7 -217
  28. package/dist/index-qMbxed1B.d.cts.map +1 -0
  29. package/dist/{index-CegM8o15.d.ts → index-wMc1E_aM.d.ts} +2 -2
  30. package/dist/{index-CegM8o15.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
  31. package/dist/index.cjs +32 -130
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +115 -97
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +115 -97
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +9 -117
  38. package/dist/index.js.map +1 -1
  39. package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
  40. package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
  41. package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
  42. package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
  43. package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
  44. package/dist/registry-Ce00A_OW.cjs.map +1 -0
  45. package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
  46. package/dist/registry-DaZRpqjW.js.map +1 -0
  47. package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
  48. package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
  49. package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
  50. package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
  51. package/dist/schemas.cjs +2 -2
  52. package/dist/schemas.d.cts +1 -1
  53. package/dist/schemas.d.ts +1 -1
  54. package/dist/schemas.js +2 -2
  55. package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
  56. package/dist/scoring-BKNNTz-s.js.map +1 -0
  57. package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
  58. package/dist/scoring-lKQVYsxy.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-NYrTlpID.cjs} +2 -2
  64. package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
  65. package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
  66. package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.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-B222dtHH.d.cts.map +0 -1
  75. package/dist/ai-B222dtHH.d.ts.map +0 -1
  76. package/dist/ai-writing-CCBjTOM3.js.map +0 -1
  77. package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
  78. package/dist/index-BWPRPIXL.d.ts.map +0 -1
  79. package/dist/index-Dyu66R4-.d.cts.map +0 -1
  80. package/dist/index-O4_ExlAD.d.cts.map +0 -1
  81. package/dist/index-UGxRSopE.d.ts.map +0 -1
  82. package/dist/registry-DVi0LR_i.cjs.map +0 -1
  83. package/dist/registry-Dsx9mNsS.js.map +0 -1
  84. package/dist/scoring-B2_HEhMT.cjs.map +0 -1
  85. package/dist/scoring-BZqUkuDF.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"item-group-DroFXgLb.cjs","names":["z","MediaSchema","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 } 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 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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"}
1
+ {"version":3,"file":"item-group-DEB7nLID.cjs","names":["z","MediaSchema","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 } 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 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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"}
@@ -1,4 +1,4 @@
1
- import { G as groupCaptionsField, W as GROUP_CAPTIONS_REVEAL_DICTATION, _t as MediaSchema, a as readSchema, t as getActivityTypeDescriptor, vt as RedactedMediaSchema, yt as maxUnits } from "./registry-Dsx9mNsS.js";
1
+ import { Ct as RedactedMediaSchema, St as MediaSchema, X as groupCaptionsField, Y as GROUP_CAPTIONS_REVEAL_DICTATION, a as readSchema, t as getActivityTypeDescriptor, wt as maxUnits } from "./registry-DaZRpqjW.js";
2
2
  import * as z from "zod/v4";
3
3
  //#region src/timeline-limits.ts
4
4
  /**
@@ -442,4 +442,4 @@ function validateItemGroup(data) {
442
442
  //#endregion
443
443
  export { INTERACTIVE_VIDEO_ITEM_TYPES as a, TIMELINE_MAX_QUIZZES as c, validateItemGroup as i, TIMELINE_MAX_TITLE_LENGTH as l, RedactedItemGroupSchema as n, TIMELINE_MAX_CHAPTERS as o, StimulusSchema as r, TIMELINE_MAX_ITEMS as s, ItemGroupSchema as t, isInteractiveVideoItemType as u };
444
444
 
445
- //# sourceMappingURL=item-group-DXiR8nOr.js.map
445
+ //# sourceMappingURL=item-group-DdSUCUjM.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"item-group-DXiR8nOr.js","names":[],"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 } 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 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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"}
1
+ {"version":3,"file":"item-group-DdSUCUjM.js","names":[],"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 } 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 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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"}
@@ -2755,6 +2755,97 @@ function countWords(text) {
2755
2755
  if (trimmed === "") return 0;
2756
2756
  return trimmed.split(/\s+/).length;
2757
2757
  }
2758
+ /** A label as labels are compared: composed, trimmed, its spacing collapsed, lower-cased. */
2759
+ function levelKey(label) {
2760
+ return label.normalize("NFC").trim().replace(/\s+/gu, " ").toLowerCase();
2761
+ }
2762
+ /**
2763
+ * The levels in `value`, or the first reason they cannot be used. `null` or
2764
+ * absent means the criterion has no levels, which is not a problem: it is
2765
+ * judged on a score.
2766
+ *
2767
+ * Each element and each field is read once, so a getter cannot pass the check
2768
+ * with one value and hand the grader another. The levels come back as written.
2769
+ */
2770
+ function readRubricLevels(value) {
2771
+ if (value === void 0 || value === null) return {
2772
+ ok: true,
2773
+ levels: null
2774
+ };
2775
+ if (!Array.isArray(value)) return {
2776
+ ok: false,
2777
+ problem: "not-a-list"
2778
+ };
2779
+ const count = value.length;
2780
+ if (count < 2) return {
2781
+ ok: false,
2782
+ problem: "too-few"
2783
+ };
2784
+ if (count > 10) return {
2785
+ ok: false,
2786
+ problem: "too-many"
2787
+ };
2788
+ const levels = [];
2789
+ const seen = /* @__PURE__ */ new Set();
2790
+ for (let index = 0; index < count; index += 1) {
2791
+ const entry = value[index];
2792
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return {
2793
+ ok: false,
2794
+ problem: "not-a-level",
2795
+ index
2796
+ };
2797
+ const record = entry;
2798
+ const label = record.label;
2799
+ const description = record.description;
2800
+ if (typeof label !== "string" || label.trim() === "") return {
2801
+ ok: false,
2802
+ problem: "no-label",
2803
+ index
2804
+ };
2805
+ if (description !== void 0 && typeof description !== "string") return {
2806
+ ok: false,
2807
+ problem: "bad-description",
2808
+ index
2809
+ };
2810
+ const key = levelKey(label);
2811
+ if (seen.has(key)) return {
2812
+ ok: false,
2813
+ problem: "duplicate-label",
2814
+ index
2815
+ };
2816
+ seen.add(key);
2817
+ levels.push({
2818
+ label,
2819
+ ...description !== void 0 ? { description } : {}
2820
+ });
2821
+ }
2822
+ return {
2823
+ ok: true,
2824
+ levels
2825
+ };
2826
+ }
2827
+ /**
2828
+ * How far level probabilities may sum from 1 and still be a distribution:
2829
+ * rounding in a grader's output, not a different judgement.
2830
+ */
2831
+ const LEVEL_PROBABILITY_TOLERANCE = 1e-6;
2832
+ /**
2833
+ * `values` as a distribution over `count` levels whose likeliest is level
2834
+ * `chosen`, a tie for likeliest included: each a finite number from 0 to 1,
2835
+ * together 1 within {@link LEVEL_PROBABILITY_TOLERANCE}. `null` when they are
2836
+ * not one.
2837
+ *
2838
+ * The AI check refuses a reply whose probabilities are not a distribution, and
2839
+ * calibration and routing read a grade's the same way, so probabilities the
2840
+ * check let through are never ignored downstream. Each element is read once,
2841
+ * a hole as `undefined`, into the copy returned.
2842
+ */
2843
+ function levelDistribution(values, count, chosen) {
2844
+ const read = Array.from(values);
2845
+ if (read.length !== count || !read.every((p) => typeof p === "number" && p >= 0 && p <= 1)) return null;
2846
+ const sum = read.reduce((total, p) => total + p, 0);
2847
+ return Math.abs(sum - 1) <= 1e-6 && read[chosen] === Math.max(...read) ? read : null;
2848
+ }
2758
2849
  //#endregion
2759
2850
  //#region src/scoring/strategies/all-or-nothing.ts
2760
2851
  /**
@@ -2971,6 +3062,7 @@ const ITEM_FINDING_SEVERITY = {
2971
3062
  gs_bank_no_distractor: "warning",
2972
3063
  gs_answer_in_passage: "warning",
2973
3064
  wr_criterion_name_duplicate: "warning",
3065
+ wr_levels_unreadable: "warning",
2974
3066
  ra_text_long_for_time: "warning",
2975
3067
  set_key_position_same: "warning"
2976
3068
  };
@@ -3221,14 +3313,38 @@ function critiqueGapSelect(draft) {
3221
3313
  });
3222
3314
  return findings;
3223
3315
  }
3316
+ /** Why a criterion's levels cannot be graded on, said to the author. */
3317
+ const LEVELS_PROBLEM = {
3318
+ "not-a-list": (criterion) => `Criterion ${criterion}'s levels are not a list`,
3319
+ "too-few": (criterion) => `Criterion ${criterion} has fewer than 2 levels`,
3320
+ "too-many": (criterion) => `Criterion ${criterion} has more than 10 levels`,
3321
+ "not-a-level": (criterion, level) => `Level ${level} of criterion ${criterion} is not a level`,
3322
+ "no-label": (criterion, level) => `Level ${level} of criterion ${criterion} has no label`,
3323
+ "bad-description": (criterion, level) => `Level ${level} of criterion ${criterion} has a description that is not text`,
3324
+ "duplicate-label": (criterion, level) => `Level ${level} of criterion ${criterion} has the label of an earlier level`
3325
+ };
3224
3326
  function critiqueWrittenResponse(draft) {
3225
3327
  const rubric = isRecord(draft.rubric) ? draft.rubric : void 0;
3226
- return duplicates(rubric === void 0 ? [] : listOf(rubric.criteria), (criterion) => isRecord(criterion) ? written(criterion.name) : void 0).map(({ first, second }) => finding("wr_criterion_name_duplicate", [
3328
+ const criteria = rubric === void 0 ? [] : listOf(rubric.criteria);
3329
+ const names = duplicates(criteria, (criterion) => isRecord(criterion) ? written(criterion.name) : void 0).map(({ first, second }) => finding("wr_criterion_name_duplicate", [
3227
3330
  "rubric",
3228
3331
  "criteria",
3229
3332
  second,
3230
3333
  "name"
3231
3334
  ], `Criteria ${ordinal(first)} and ${ordinal(second)} have the same name. Feedback and grades name a criterion, so one of them cannot be told apart.`));
3335
+ const levels = criteria.flatMap((criterion, index) => {
3336
+ if (!isRecord(criterion)) return [];
3337
+ const read = readRubricLevels(criterion.levels);
3338
+ if (read.ok) return [];
3339
+ const level = ordinal(read.index ?? 0);
3340
+ return [finding("wr_levels_unreadable", [
3341
+ "rubric",
3342
+ "criteria",
3343
+ index,
3344
+ "levels"
3345
+ ], `${LEVELS_PROBLEM[read.problem](ordinal(index), level)}, so a grader cannot judge against them.`)];
3346
+ });
3347
+ return [...names, ...levels];
3232
3348
  }
3233
3349
  /** 150 words a minute: a fluent adult reading aloud. A learner is slower. */
3234
3350
  const WORDS_PER_SECOND = 2.5;
@@ -4410,7 +4526,11 @@ const WrittenResponseRubricSchema = zod_v4.looseObject({
4410
4526
  const RedactedWrittenResponseRubricCriterionSchema = zod_v4.strictObject({
4411
4527
  name: zod_v4.string().min(1),
4412
4528
  description: zod_v4.string().optional(),
4413
- weight: zod_v4.number().min(0)
4529
+ weight: zod_v4.number().min(0),
4530
+ levels: zod_v4.array(zod_v4.strictObject({
4531
+ label: zod_v4.string().min(1),
4532
+ description: zod_v4.string().optional()
4533
+ })).min(2).max(10).optional()
4414
4534
  });
4415
4535
  const RedactedWrittenResponseRubricSchema = zod_v4.strictObject({
4416
4536
  label: zod_v4.string().optional(),
@@ -4852,6 +4972,32 @@ function defineActivityType(descriptor) {
4852
4972
  return descriptor;
4853
4973
  }
4854
4974
  //#endregion
4975
+ //#region src/registry/keep-when.ts
4976
+ /**
4977
+ * A nested field policy that keeps its field only when the field's value
4978
+ * passes a test. Internal: the symbol is never exported, so no host policy can
4979
+ * carry one, and `FieldPolicy`'s string keys never see it.
4980
+ *
4981
+ * It can only drop a field the policy would otherwise emit, never emit one it
4982
+ * would drop, so redaction stays fail-closed. It exists for a field
4983
+ * `validateActivity` does not check in 1.x — a rubric criterion's `levels` —
4984
+ * whose malformed values would otherwise reach the strict redacted schema and
4985
+ * make `redact()` throw on content it accepted before.
4986
+ */
4987
+ const KEEP_WHEN = Symbol("lk.keepWhen");
4988
+ /** `policy`, kept only for values `test` accepts. */
4989
+ function keepWhen(policy, test) {
4990
+ const guarded = {
4991
+ ...policy,
4992
+ [KEEP_WHEN]: test
4993
+ };
4994
+ return Object.freeze(guarded);
4995
+ }
4996
+ /** The test a nested policy keeps its field by, when it has one. */
4997
+ function guardOf(policy) {
4998
+ return policy[KEEP_WHEN];
4999
+ }
5000
+ //#endregion
4855
5001
  //#region src/registry/builtins.ts
4856
5002
  /**
4857
5003
  * Field-sensitivity policies for the built-in types (R7). Fail-closed:
@@ -5069,7 +5215,11 @@ const RUBRIC_FIELD_POLICY = {
5069
5215
  criteria: {
5070
5216
  name: "public",
5071
5217
  description: "public",
5072
- weight: "public"
5218
+ weight: "public",
5219
+ levels: keepWhen({
5220
+ label: "public",
5221
+ description: "public"
5222
+ }, (value) => readRubricLevels(value).ok)
5073
5223
  }
5074
5224
  };
5075
5225
  const WRITTEN_RESPONSE_FIELD_POLICY = {
@@ -5551,6 +5701,12 @@ Object.defineProperty(exports, "GapSelectDataSchema", {
5551
5701
  return GapSelectDataSchema;
5552
5702
  }
5553
5703
  });
5704
+ Object.defineProperty(exports, "LEVEL_PROBABILITY_TOLERANCE", {
5705
+ enumerable: true,
5706
+ get: function() {
5707
+ return LEVEL_PROBABILITY_TOLERANCE;
5708
+ }
5709
+ });
5554
5710
  Object.defineProperty(exports, "MEDIA_FIELD_POLICY", {
5555
5711
  enumerable: true,
5556
5712
  get: function() {
@@ -5755,6 +5911,12 @@ Object.defineProperty(exports, "groupCaptionsField", {
5755
5911
  return groupCaptionsField;
5756
5912
  }
5757
5913
  });
5914
+ Object.defineProperty(exports, "guardOf", {
5915
+ enumerable: true,
5916
+ get: function() {
5917
+ return guardOf;
5918
+ }
5919
+ });
5758
5920
  Object.defineProperty(exports, "isBuiltInActivityType", {
5759
5921
  enumerable: true,
5760
5922
  get: function() {
@@ -5803,6 +5965,18 @@ Object.defineProperty(exports, "keyPositionFinding", {
5803
5965
  return keyPositionFinding;
5804
5966
  }
5805
5967
  });
5968
+ Object.defineProperty(exports, "levelDistribution", {
5969
+ enumerable: true,
5970
+ get: function() {
5971
+ return levelDistribution;
5972
+ }
5973
+ });
5974
+ Object.defineProperty(exports, "levelKey", {
5975
+ enumerable: true,
5976
+ get: function() {
5977
+ return levelKey;
5978
+ }
5979
+ });
5806
5980
  Object.defineProperty(exports, "levenshteinDistance", {
5807
5981
  enumerable: true,
5808
5982
  get: function() {
@@ -5863,6 +6037,12 @@ Object.defineProperty(exports, "readAloudType", {
5863
6037
  return readAloudType;
5864
6038
  }
5865
6039
  });
6040
+ Object.defineProperty(exports, "readRubricLevels", {
6041
+ enumerable: true,
6042
+ get: function() {
6043
+ return readRubricLevels;
6044
+ }
6045
+ });
5866
6046
  Object.defineProperty(exports, "readSchema", {
5867
6047
  enumerable: true,
5868
6048
  get: function() {
@@ -5912,4 +6092,4 @@ Object.defineProperty(exports, "writtenResponseType", {
5912
6092
  }
5913
6093
  });
5914
6094
 
5915
- //# sourceMappingURL=registry-DVi0LR_i.cjs.map
6095
+ //# sourceMappingURL=registry-Ce00A_OW.cjs.map