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