@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.
- package/CHANGELOG.md +59 -0
- package/README.md +2 -0
- package/dist/{ai-B222dtHH.d.ts → ai-BYryFkF9.d.cts} +462 -5
- package/dist/ai-BYryFkF9.d.cts.map +1 -0
- package/dist/{ai-B222dtHH.d.cts → ai-BYryFkF9.d.ts} +462 -5
- package/dist/ai-BYryFkF9.d.ts.map +1 -0
- package/dist/ai-check.cjs +274 -23
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +13 -5
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +13 -5
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +274 -23
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-Cen7-3qs.cjs} +520 -58
- package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
- package/dist/{ai-writing-CCBjTOM3.js → ai-writing-D8PdhJJz.js} +513 -75
- package/dist/ai-writing-D8PdhJJz.js.map +1 -0
- package/dist/{index-Dyu66R4-.d.cts → index-Bg0aeWtM.d.cts} +327 -3
- package/dist/index-Bg0aeWtM.d.cts.map +1 -0
- package/dist/{index-BWPRPIXL.d.ts → index-CxpiWedW.d.ts} +7 -217
- package/dist/index-CxpiWedW.d.ts.map +1 -0
- package/dist/{index-CXyOV8sm.d.cts → index-D7R3Ll8R.d.cts} +2 -2
- package/dist/{index-CXyOV8sm.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
- package/dist/{index-UGxRSopE.d.ts → index-DUaW1Tj5.d.ts} +327 -3
- package/dist/index-DUaW1Tj5.d.ts.map +1 -0
- package/dist/{index-O4_ExlAD.d.cts → index-qMbxed1B.d.cts} +7 -217
- package/dist/index-qMbxed1B.d.cts.map +1 -0
- package/dist/{index-CegM8o15.d.ts → index-wMc1E_aM.d.ts} +2 -2
- package/dist/{index-CegM8o15.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
- package/dist/index.cjs +32 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -97
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +115 -97
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -117
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
- package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
- package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
- package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
- package/dist/registry-Ce00A_OW.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
- package/dist/registry-DaZRpqjW.js.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
- package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
- package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
- package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
- package/dist/schemas.cjs +2 -2
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +2 -2
- package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
- package/dist/scoring-BKNNTz-s.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
- package/dist/scoring-lKQVYsxy.cjs.map +1 -0
- package/dist/scoring.cjs +8 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-DNzkSJ0S.cjs → xapi-NYrTlpID.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -2
- package/package.json +2 -2
- package/vectors/README.md +24 -1
- package/vectors/scoring.json +18182 -1
- package/dist/ai-B222dtHH.d.cts.map +0 -1
- package/dist/ai-B222dtHH.d.ts.map +0 -1
- package/dist/ai-writing-CCBjTOM3.js.map +0 -1
- package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
- package/dist/index-BWPRPIXL.d.ts.map +0 -1
- package/dist/index-Dyu66R4-.d.cts.map +0 -1
- package/dist/index-O4_ExlAD.d.cts.map +0 -1
- package/dist/index-UGxRSopE.d.ts.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.map +0 -1
- package/dist/scoring-B2_HEhMT.cjs.map +0 -1
- 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 {
|
|
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-
|
|
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
|
-
|
|
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-
|
|
6095
|
+
//# sourceMappingURL=registry-Ce00A_OW.cjs.map
|