@intellectif/lk-core 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +2 -0
  3. package/dist/ai-check.cjs +175 -17
  4. package/dist/ai-check.cjs.map +1 -1
  5. package/dist/ai-check.d.cts +13 -5
  6. package/dist/ai-check.d.cts.map +1 -1
  7. package/dist/ai-check.d.ts +13 -5
  8. package/dist/ai-check.d.ts.map +1 -1
  9. package/dist/ai-check.js +175 -17
  10. package/dist/ai-check.js.map +1 -1
  11. package/dist/{ai-CnmLPMzd.d.ts → ai-i3jwXnpX.d.cts} +242 -7
  12. package/dist/ai-i3jwXnpX.d.cts.map +1 -0
  13. package/dist/{ai-CnmLPMzd.d.cts → ai-i3jwXnpX.d.ts} +242 -7
  14. package/dist/ai-i3jwXnpX.d.ts.map +1 -0
  15. package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-C4YRVrFi.cjs} +683 -72
  16. package/dist/ai-writing-C4YRVrFi.cjs.map +1 -0
  17. package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-OJ5m_1EB.js} +663 -88
  18. package/dist/ai-writing-OJ5m_1EB.js.map +1 -0
  19. package/dist/{index-BqMXWuNC.d.cts → index--U7hHV0s.d.cts} +2 -2
  20. package/dist/{index-BqMXWuNC.d.cts.map → index--U7hHV0s.d.cts.map} +1 -1
  21. package/dist/{index-B54baFUA.d.ts → index-CFRgZLhH.d.cts} +45 -12
  22. package/dist/index-CFRgZLhH.d.cts.map +1 -0
  23. package/dist/{index-CVWvymVb.d.ts → index-DeD8EI1D.d.ts} +327 -3
  24. package/dist/index-DeD8EI1D.d.ts.map +1 -0
  25. package/dist/{index-CcHDTxIZ.d.ts → index-Dj92T76o.d.ts} +2 -2
  26. package/dist/{index-CcHDTxIZ.d.ts.map → index-Dj92T76o.d.ts.map} +1 -1
  27. package/dist/{index-BAfEHCnD.d.cts → index-Uh1u57yq.d.cts} +327 -3
  28. package/dist/index-Uh1u57yq.d.cts.map +1 -0
  29. package/dist/{index-2nigSf4N.d.cts → index-WBHSrIUU.d.ts} +45 -12
  30. package/dist/index-WBHSrIUU.d.ts.map +1 -0
  31. package/dist/index.cjs +88 -131
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +217 -95
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +217 -95
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +60 -118
  38. package/dist/index.js.map +1 -1
  39. package/dist/{item-group-DXiR8nOr.js → item-group-BbvvH_Ob.js} +3 -2
  40. package/dist/item-group-BbvvH_Ob.js.map +1 -0
  41. package/dist/{item-group-DroFXgLb.cjs → item-group-CQRbb8Rx.cjs} +3 -2
  42. package/dist/item-group-CQRbb8Rx.cjs.map +1 -0
  43. package/dist/{registry-Dsx9mNsS.js → registry-B23o9UWh.js} +218 -21
  44. package/dist/registry-B23o9UWh.js.map +1 -0
  45. package/dist/{registry-DVi0LR_i.cjs → registry-Ih5n4PVZ.cjs} +265 -20
  46. package/dist/registry-Ih5n4PVZ.cjs.map +1 -0
  47. package/dist/{schemas-DVbAOrtd.cjs → schemas-Q2dyOgc2.cjs} +77 -14
  48. package/dist/schemas-Q2dyOgc2.cjs.map +1 -0
  49. package/dist/{schemas-Bdw0nGPo.js → schemas-yId-bJhD.js} +72 -15
  50. package/dist/schemas-yId-bJhD.js.map +1 -0
  51. package/dist/schemas.cjs +5 -2
  52. package/dist/schemas.d.cts +2 -2
  53. package/dist/schemas.d.ts +2 -2
  54. package/dist/schemas.js +4 -3
  55. package/dist/{scoring-BZqUkuDF.js → scoring-CLMdk8i9.js} +657 -3
  56. package/dist/scoring-CLMdk8i9.js.map +1 -0
  57. package/dist/{scoring-B2_HEhMT.cjs → scoring-CaPS6q8b.cjs} +722 -2
  58. package/dist/scoring-CaPS6q8b.cjs.map +1 -0
  59. package/dist/scoring.cjs +8 -2
  60. package/dist/scoring.d.cts +3 -3
  61. package/dist/scoring.d.ts +3 -3
  62. package/dist/scoring.js +3 -3
  63. package/dist/{xapi-DNzkSJ0S.cjs → xapi-BX0hvRRB.cjs} +2 -2
  64. package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-BX0hvRRB.cjs.map} +1 -1
  65. package/dist/{xapi-CMTph7wG.js → xapi-Cdk9fTvV.js} +2 -2
  66. package/dist/{xapi-CMTph7wG.js.map → xapi-Cdk9fTvV.js.map} +1 -1
  67. package/dist/xapi.cjs +1 -1
  68. package/dist/xapi.d.cts +1 -1
  69. package/dist/xapi.d.ts +1 -1
  70. package/dist/xapi.js +1 -2
  71. package/package.json +2 -2
  72. package/vectors/README.md +24 -1
  73. package/vectors/scoring.json +18182 -1
  74. package/dist/ai-CnmLPMzd.d.cts.map +0 -1
  75. package/dist/ai-CnmLPMzd.d.ts.map +0 -1
  76. package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
  77. package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
  78. package/dist/index-2nigSf4N.d.cts.map +0 -1
  79. package/dist/index-B54baFUA.d.ts.map +0 -1
  80. package/dist/index-BAfEHCnD.d.cts.map +0 -1
  81. package/dist/index-CVWvymVb.d.ts.map +0 -1
  82. package/dist/item-group-DXiR8nOr.js.map +0 -1
  83. package/dist/item-group-DroFXgLb.cjs.map +0 -1
  84. package/dist/registry-DVi0LR_i.cjs.map +0 -1
  85. package/dist/registry-Dsx9mNsS.js.map +0 -1
  86. package/dist/schemas-Bdw0nGPo.js.map +0 -1
  87. package/dist/schemas-DVbAOrtd.cjs.map +0 -1
  88. package/dist/scoring-B2_HEhMT.cjs.map +0 -1
  89. package/dist/scoring-BZqUkuDF.js.map +0 -1
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/content-hash.ts","../src/delivery.ts","../src/shuffle.ts","../src/item-group.ts","../src/media-budget.ts","../src/attempt-plan.ts","../src/attempt-state.ts","../src/redact.ts","../src/timeline.ts"],"sourcesContent":["/**\n * Content fingerprinting — \"is this the same content the learner was served?\"\n *\n * A recorded attempt outlives the content it was taken against. Papers get\n * corrected, a typo is fixed, an option is reworded — and six months later a\n * remark, an appeal, or a per-item analysis is run against content that is no\n * longer what the learner saw. Nothing warns anybody, because the ids all still\n * match. A fingerprint stored with the attempt turns that silent drift into a\n * question somebody can answer.\n */\n\n/**\n * Deterministic JSON: object keys sorted, arrays left in order, `undefined`\n * omitted from objects (matching `JSON.stringify`) and rendered as `null`\n * inside arrays.\n *\n * Key order is the whole point. Two objects that differ only in the order\n * their keys happened to be written are the SAME content, and a fingerprint\n * that disagreed would raise a false alarm every time a payload made a\n * round-trip through a different serializer.\n */\nexport function canonicalJson(value: unknown, seen: Set<object> = new Set()): string {\n if (value === null) {\n return 'null';\n }\n if (typeof value === 'number') {\n // JSON.stringify renders every non-finite number as `null`, which would\n // fingerprint NaN, Infinity and -Infinity identically — and identically to\n // a real null. Content carrying one of those is already broken; it must at\n // least be distinguishable.\n return Number.isFinite(value) ? JSON.stringify(value === 0 ? 0 : value) : `\"#${String(value)}\"`;\n }\n if (typeof value === 'string' || typeof value === 'boolean') {\n return JSON.stringify(value);\n }\n if (typeof value === 'bigint') {\n return `\"#${value.toString()}n\"`;\n }\n if (typeof value === 'object') {\n // A cycle would recurse until the stack gave out, and a stack overflow\n // while fingerprinting an exam is a far worse failure than being told the\n // content is not serialisable. Content is JSON, so this should never fire —\n // but \"should never\" is not a guard.\n if (seen.has(value)) {\n throw new Error(\n 'canonicalJson: the value contains a circular reference and cannot be fingerprinted.',\n );\n }\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n return `[${value.map((element) => (element === undefined ? 'null' : canonicalJson(element, seen))).join(',')}]`;\n }\n const source = value as Record<string, unknown>;\n const parts: string[] = [];\n for (const key of Object.keys(source).sort()) {\n const entry = source[key];\n if (entry === undefined) {\n continue;\n }\n parts.push(`${JSON.stringify(key)}:${canonicalJson(entry, seen)}`);\n }\n return `{${parts.join(',')}}`;\n } finally {\n // Released on the way out, so a value legitimately appearing twice in\n // SIBLING positions is not mistaken for a cycle.\n seen.delete(value);\n }\n }\n // Functions, symbols and undefined have no place in content.\n return 'null';\n}\n\nconst FNV_OFFSET = 14695981039346656037n;\nconst FNV_PRIME = 1099511628211n;\nconst MASK64 = (1n << 64n) - 1n;\n\n/**\n * FNV-1a over the UTF-8 bytes of `input`, as 16 lowercase hex digits.\n *\n * **This is a change-detection fingerprint, not a tamper-evident signature.**\n * It is deterministic across runtimes and dependency-free, which is what makes\n * it usable in a stored grade record — but an adversary who can edit content\n * can also, with effort, preserve the fingerprint. If you need the stronger\n * property, sign the plan with a key the content author does not hold; this\n * exists to catch honest edits, which is what actually happens.\n */\nexport function fingerprint(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let hash = FNV_OFFSET;\n for (const byte of bytes) {\n hash = ((hash ^ BigInt(byte)) * FNV_PRIME) & MASK64;\n }\n return hash.toString(16).padStart(16, '0');\n}\n\n/**\n * Fingerprints any JSON-serialisable value through {@link canonicalJson}, so\n * the result depends on the content and not on how it was written.\n */\nexport function contentHash(value: unknown): string {\n return fingerprint(canonicalJson(value));\n}\n","import type {\n DeliveryPolicy,\n DeliveryPolicyIssue,\n ResolvedDeliveryPolicy,\n} from './types/delivery.js';\n\n/**\n * The policy that restricts nothing: every setting `true`. It is what an absent\n * policy resolves to, and it is exactly the behaviour the SDK had before\n * delivery policies existed.\n */\nexport const OPEN_DELIVERY_POLICY: ResolvedDeliveryPolicy = Object.freeze({\n feedback: true,\n solutions: true,\n hints: true,\n ai: Object.freeze({ hints: true, explanations: true }),\n}) as ResolvedDeliveryPolicy;\n\nconst SETTINGS = ['feedback', 'solutions', 'hints'] as const;\nconst AI_SETTINGS = ['hints', 'explanations'] as const;\n\n/**\n * One setting as a render reads it. Absent or `null` is unset, and unset is\n * `true`: JSON has no `undefined`, so a setting nobody chose arrives from a\n * database as `null`.\n *\n * Anything else that is not exactly `true` reads as `false`. Every setting is a\n * restriction, so a value that cannot be read is read as the restriction — a\n * `\"false\"` string from a form, say, whose author meant exactly that. Reading\n * it as `true` would put hints on a paper whose school had switched them off.\n */\nfunction allows(value: unknown): boolean {\n return value === undefined || value === null || value === true;\n}\n\n/**\n * A policy with every setting spelled out — see {@link DeliveryPolicy}.\n *\n * Tolerant, because it runs where a learner is waiting: nothing here throws.\n * A value it cannot read restricts rather than allows (see above), and a\n * policy that is not an object at all restricts everything. Check a policy\n * before you store it with {@link validateDeliveryPolicy}; `planAttempt` does.\n */\nexport function resolveDeliveryPolicy(policy: unknown): ResolvedDeliveryPolicy {\n if (policy === undefined || policy === null) {\n return OPEN_DELIVERY_POLICY;\n }\n if (typeof policy !== 'object' || Array.isArray(policy)) {\n return {\n feedback: false,\n solutions: false,\n hints: false,\n ai: { hints: false, explanations: false },\n };\n }\n const given = policy as Record<string, unknown>;\n const ai = given.ai;\n // `true` and `false` are shorthand for both AI settings at once. `true`\n // restricts nothing, which is also what leaving `ai` out does — so reading it\n // generously cannot switch on anything a school did not already allow.\n const aiGiven =\n ai === undefined || ai === null || ai === true\n ? {}\n : typeof ai === 'object' && !Array.isArray(ai)\n ? (ai as Record<string, unknown>)\n : { hints: false, explanations: false };\n return {\n feedback: allows(given.feedback),\n solutions: allows(given.solutions),\n hints: allows(given.hints),\n ai: { hints: allows(aiGiven.hints), explanations: allows(aiGiven.explanations) },\n };\n}\n\n/**\n * The strictest of several policies: a setting is on only where every one of\n * them leaves it on. How a course's policy and an assessment's combine, and\n * how a pager's policy wins over whatever a question it draws was handed.\n */\nexport function combineDeliveryPolicies(...policies: unknown[]): ResolvedDeliveryPolicy {\n const resolved = policies.map(resolveDeliveryPolicy);\n const every = (read: (one: ResolvedDeliveryPolicy) => boolean): boolean => resolved.every(read);\n return {\n feedback: every((one) => one.feedback),\n solutions: every((one) => one.solutions),\n hints: every((one) => one.hints),\n ai: {\n hints: every((one) => one.ai.hints),\n explanations: every((one) => one.ai.explanations),\n },\n };\n}\n\n/**\n * Checks a policy before it is stored: each setting `true`, `false`, `null`\n * or absent, and nothing else — no misspelled setting, no string standing in\n * for a boolean.\n *\n * Stricter than {@link resolveDeliveryPolicy} on purpose. A misspelled\n * restriction (`hint: false`) is silently no restriction at all, and a\n * policy is a record of the conditions a paper was sat under: it is worth\n * refusing before it is written down, rather than read charitably after.\n */\nexport function validateDeliveryPolicy(\n policy: unknown,\n): { success: true; data: DeliveryPolicy } | { success: false; issues: DeliveryPolicyIssue[] } {\n const issues: DeliveryPolicyIssue[] = [];\n if (policy === undefined || policy === null) {\n return { success: true, data: {} };\n }\n if (typeof policy !== 'object' || Array.isArray(policy)) {\n return {\n success: false,\n issues: [{ path: '', message: 'A delivery policy is an object of settings.' }],\n };\n }\n const given = policy as Record<string, unknown>;\n for (const key of Object.keys(given)) {\n if (key === 'ai') {\n continue;\n }\n if (!(SETTINGS as readonly string[]).includes(key)) {\n issues.push({\n path: key,\n message: `\"${key}\" is not a delivery setting. The settings are feedback, solutions, hints and ai.`,\n });\n continue;\n }\n const value = given[key];\n if (value !== true && value !== false && value !== null && value !== undefined) {\n issues.push({ path: key, message: `\"${key}\" must be true, false or null.` });\n }\n }\n const ai = given.ai;\n if (ai !== undefined && ai !== null && ai !== true && ai !== false) {\n if (typeof ai !== 'object' || Array.isArray(ai)) {\n issues.push({\n path: 'ai',\n message: '\"ai\" must be true, false, or an object of hints and explanations.',\n });\n } else {\n for (const [key, value] of Object.entries(ai as Record<string, unknown>)) {\n if (!(AI_SETTINGS as readonly string[]).includes(key)) {\n issues.push({\n path: `ai.${key}`,\n message: `\"ai.${key}\" is not an AI setting. The settings are hints and explanations.`,\n });\n continue;\n }\n if (value !== true && value !== false && value !== null && value !== undefined) {\n issues.push({ path: `ai.${key}`, message: `\"ai.${key}\" must be true, false or null.` });\n }\n }\n }\n }\n return issues.length > 0\n ? { success: false, issues }\n : { success: true, data: given as DeliveryPolicy };\n}\n","/**\n * Seeded, deterministic shuffling — the ONE algorithm every presentation-order\n * decision in the SDK goes through: option order in a multiple-choice item,\n * item order inside an item group, entry order in a sequence. A server and a\n * client holding the same seed derive the same order, and an order recorded\n * against an attempt can be rebuilt later from nothing but its seed.\n *\n * STABILITY: the hash and the generator are part of the wire contract. A\n * stored attempt may hold only a seed and rely on this function to reproduce\n * the order it presented; changing either would silently re-order every\n * recorded attempt. Any change here is a package major, and the pinned\n * permutation test exists so an accidental one fails loudly.\n */\n\n/** FNV-1a hash of a string → unsigned 32-bit integer. */\nexport function hashSeed(input: string): number {\n let h = 2166136261 >>> 0;\n for (let i = 0; i < input.length; i += 1) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 16777619) >>> 0;\n }\n return h;\n}\n\n/**\n * Which draw the Fisher–Yates step uses.\n *\n * - `1` — the original. Takes the index from the LCG's LOW bits (`s % (i+1)`).\n * - `2` — takes it from the HIGH bits. Same LCG, same seed, different draw.\n *\n * The difference is not cosmetic. In a linear congruential generator with a\n * power-of-two modulus, the low bits have drastically short periods: bit 0\n * alternates, bit 1 has period 4, and so on. `% (i+1)` reads exactly those\n * bits, so consecutive draws are correlated and most permutations become\n * unreachable FOR EVERY SEED THAT WILL EVER EXIST:\n *\n * | items | permutations | reachable under v1 | under v2 |\n * | ----- | ------------ | ------------------ | -------- |\n * | 4 | 24 | 12 | 24 |\n * | 5 | 120 | 60 | 120 |\n * | 6 | 720 | 180 | 720 |\n *\n * It also biases WHERE an option lands. On a four-option item under v1 the\n * last authored option takes the first presented position 8.3% of the time\n * and the second position 41.7%, against 25% each — a systematic advantage to\n * anyone who notices. For a summative exam that is a fairness defect, not a\n * curiosity.\n *\n * v1 remains the DEFAULT regardless, because these permutations are a wire\n * contract: an attempt may be stored with nothing but its seed, and a review\n * render of that attempt has to reproduce the order the learner actually saw.\n * Switching the default would silently re-order every recorded attempt, so it\n * is a package major — scheduled, not smuggled in. Choose v2 for new content\n * where no attempt has been recorded yet.\n */\nexport type ShuffleVersion = 1 | 2;\n\n/** Options for {@link seededShuffle}. */\nexport interface SeededShuffleOptions {\n /**\n * Draw algorithm — see {@link ShuffleVersion}. Left out, it is `1`; leaving\n * it out is deprecated since lk-core 1.3, since a 2.0 may make `2` the\n * default. Name it: `1` to reproduce an order already recorded, `2` for new\n * content.\n */\n version?: ShuffleVersion;\n}\n\n/** LCG-driven Fisher–Yates over a 32-bit seed; returns a permutation (no loss, no duplicate). */\nfunction shuffleWithSeed<T>(items: readonly T[], seed: number, version: ShuffleVersion): T[] {\n const out = [...items];\n let s = seed >>> 0 || 1;\n for (let i = out.length - 1; i > 0; i -= 1) {\n s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n // v1 reads the low bits; v2 scales the whole 32-bit word, which uses the\n // high bits and reaches every permutation.\n const j = version === 2 ? Math.floor((s / 4294967296) * (i + 1)) : s % (i + 1);\n [out[i], out[j]] = [out[j] as T, out[i] as T];\n }\n return out;\n}\n\n/**\n * Returns a new array holding a permutation of `items` determined entirely by\n * `seed` (and the chosen {@link ShuffleVersion}): same inputs, same order —\n * across processes, runtimes and releases. The input is never mutated.\n *\n * Compose the seed from the attempt identity AND the thing being shuffled\n * (`${attemptId}:${itemId}`), so two shuffles in one attempt do not share an\n * order.\n */\nexport function seededShuffle<T>(\n items: readonly T[],\n seed: string,\n options: SeededShuffleOptions & { version: ShuffleVersion },\n): T[];\n/**\n * @deprecated Since lk-core 1.3, name the version: `{ version: 1 }` draws\n * exactly what this does, `{ version: 2 }` reaches every order. A 2.0 may make\n * `2` the default, which would re-order what this call returns.\n */\nexport function seededShuffle<T>(\n items: readonly T[],\n seed: string,\n options?: SeededShuffleOptions,\n): T[];\nexport function seededShuffle<T>(\n items: readonly T[],\n seed: string,\n options: SeededShuffleOptions = {},\n): T[] {\n return shuffleWithSeed(items, hashSeed(seed), options.version ?? 1);\n}\n","import { seededShuffle } from './shuffle.js';\nimport type { ItemGroup, MediaTimeline, SequenceEntry, SequenceSlot } from './types/item-group.js';\n\n/** Narrows a sequence entry to an item group. */\nexport function isItemGroup<TItem extends { type: string }>(\n entry: SequenceEntry<TItem>,\n): entry is ItemGroup<TItem> {\n return entry.type === 'item-group';\n}\n\n/**\n * An entry's declared `slotKey`, if it has one. Read structurally rather than\n * from the type, because a plain activity may carry one too — `slotKey` is a\n * property of an item's PLACE in a paper, so any entry can declare it without\n * every activity schema having to know about assessment assembly.\n *\n * A key containing `.` is rejected: `.` separates a group from its item in a\n * slot id, so `\"a.b\"` as a group key would be indistinguishable from item `b`\n * of group `a`.\n */\nfunction keyOf(entry: unknown): string | undefined {\n const key = (entry as { slotKey?: unknown }).slotKey;\n if (key === undefined) {\n return undefined;\n }\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `flattenSequence: slotKey must be a non-empty string, received ${String(key)}.`,\n );\n }\n if (key.includes('.')) {\n throw new Error(\n `flattenSequence: slotKey \"${key}\" contains a \".\" , which separates a group from its item ` +\n 'in a slot id. Choose a key without it, or the two become indistinguishable.',\n );\n }\n return key;\n}\n\n/**\n * An interactive video's items in the order its quizzes open: by the quiz's\n * time — stably, so quizzes at one moment keep their authored order — then by\n * position within the quiz. An item no quiz places (content the schema would\n * refuse) keeps its authored place after every placed one, rather than\n * vanishing from the paper.\n */\nfunction inQuizOrder<TItem extends { id: string }>(\n items: readonly { item: TItem; itemIndex: number }[],\n timeline: MediaTimeline,\n): { item: TItem; itemIndex: number }[] {\n const placement = new Map<string, { at: number; cue: number; position: number }>();\n timeline.cues.forEach((cue, cueIndex) => {\n cue.itemIds.forEach((itemId, position) => {\n if (!placement.has(itemId)) {\n placement.set(itemId, { at: cue.at, cue: cueIndex, position });\n }\n });\n });\n const unplaced = { at: Number.POSITIVE_INFINITY, cue: Number.POSITIVE_INFINITY, position: 0 };\n return items\n .map((entry) => ({ entry, place: placement.get(entry.item.id) ?? unplaced }))\n .sort(\n (a, b) =>\n a.place.at - b.place.at ||\n a.place.cue - b.place.cue ||\n a.place.position - b.place.position ||\n a.entry.itemIndex - b.entry.itemIndex,\n )\n .map(({ entry }) => entry);\n}\n\n/** Options for {@link flattenSequence}. */\nexport interface FlattenSequenceOptions {\n /**\n * Shuffle the top-level entries. A group moves as ONE block — its items are\n * never interleaved with other entries — which is the reason a group exists\n * as a container rather than as a flag on each item.\n */\n shuffleEntries?: boolean;\n /**\n * Seed for every shuffle in this call: the entries, and each group whose\n * `shuffle` is `within-group`. REQUIRED whenever anything shuffles. The SDK\n * never invents one, because the server that stores an attempt and the\n * client that renders it must derive the SAME order, and only a shared seed\n * makes that true. The attempt id is the natural choice.\n */\n seed?: string;\n}\n\n/**\n * Turns a sequence definition — loose activities and item groups, in authored\n * order — into the ordered list of slots to present. Pure and deterministic:\n * call it on the server to record an attempt's order, and on the client to\n * render it, and the two agree.\n *\n * Shuffling is opt-in and seeded. Groups are shuffle-atomic: with\n * `shuffleEntries` a group changes position but stays one contiguous block,\n * and only a group that declares `shuffle: 'within-group'` has its items\n * reordered. Every slot carries a `slotId` derived from the authored\n * position, so identities survive shuffling.\n *\n * @throws Error when a shuffle is requested without a `seed`.\n */\nexport function flattenSequence<TItem extends { id: string; type: string }>(\n entries: readonly SequenceEntry<TItem>[],\n options: FlattenSequenceOptions = {},\n): SequenceSlot<TItem>[] {\n const shuffleEntries = options.shuffleEntries === true;\n const needsSeed =\n shuffleEntries ||\n entries.some((entry) => isItemGroup(entry) && entry.shuffle === 'within-group');\n if (needsSeed && options.seed === undefined) {\n throw new Error(\n 'flattenSequence: a seed is required when shuffling (shuffleEntries, or a group with ' +\n 'shuffle: \"within-group\"). Pass the attempt id so the server and the client derive the same order.',\n );\n }\n const seed = options.seed ?? '';\n\n // Carry the AUTHORED index through the shuffle: slot identity comes from\n // where an entry was written, never from where it happens to be shown.\n const authored = entries.map((entry, entryIndex) => ({ entry, entryIndex }));\n const ordered = shuffleEntries\n ? seededShuffle(authored, `${seed}:entries`, { version: 1 })\n : authored;\n\n const slots: SequenceSlot<TItem>[] = [];\n for (const { entry, entryIndex } of ordered) {\n // An authored `slotKey` wins over the positional path. A positional id is\n // only valid against ONE version of the entries array; a declared key\n // survives insertion, deletion and re-ordering, which is what makes a\n // stored slot id safe to re-grade against later.\n const entryKey = keyOf(entry) ?? String(entryIndex);\n if (!isItemGroup(entry)) {\n slots.push({ slotId: entryKey, index: slots.length, activity: entry });\n continue;\n }\n // An empty group contributes no slots, so it would DISAPPEAR — stimulus,\n // questions and all — from a sequence that still looks well-formed. A\n // listening section filtered to nothing upstream would leave an 18-slot\n // paper presented as 12 slots, and `composeAssessmentScore` would then\n // report a `final` grade over the survivors with nothing pending. The\n // type already declares `items` non-empty; this makes that enforceable at\n // the point where the omission would otherwise become invisible.\n if (entry.items.length === 0) {\n throw new Error(\n `flattenSequence: item group \"${entry.id}\" has no items. An empty group would silently ` +\n 'remove its stimulus and its questions from the presented sequence.',\n );\n }\n const timeline = entry.timeline;\n // A defence behind the schema's refusal: the two orders cannot both hold,\n // and a seeded shuffle of a video's questions would detach each one from\n // the moment it asks about.\n if (timeline !== undefined && entry.shuffle === 'within-group') {\n throw new Error(\n `flattenSequence: item group \"${entry.id}\" has a timeline and shuffle: \"within-group\". ` +\n 'An interactive video presents its questions in the order its quizzes open.',\n );\n }\n const items = entry.items.map((item, itemIndex) => ({ item, itemIndex }));\n const presented =\n timeline !== undefined\n ? inQuizOrder(items, timeline)\n : entry.shuffle === 'within-group'\n ? seededShuffle(items, `${seed}:group:${entry.id}`, { version: 1 })\n : items;\n const size = presented.length;\n presented.forEach(({ item, itemIndex }, position) => {\n const cue = timeline?.cues.find((candidate) => candidate.itemIds.includes(item.id));\n slots.push({\n // Slot ids stay AUTHORED — the item's index or its key — whatever the\n // presented order: moving a quiz must never re-map a stored answer.\n slotId: `${entryKey}.${keyOf(item) ?? String(itemIndex)}`,\n index: slots.length,\n activity: item,\n group: {\n id: entry.id,\n ...(entry.title !== undefined ? { title: entry.title } : {}),\n stimulus: entry.stimulus,\n position,\n size,\n ...(timeline !== undefined ? { timeline } : {}),\n ...(cue !== undefined ? { cue } : {}),\n },\n });\n });\n }\n\n // Positional ids are unique by construction; authored keys are not. Two\n // entries sharing a key would collapse into one identity, so every response\n // stored against it would overwrite the other's — and `composeAssessmentScore`\n // would score one question twice and the other never.\n const seenSlotIds = new Set<string>();\n // ENTRY keys are checked separately, because a loose entry keyed \"reading\"\n // and a group keyed \"reading\" produce slot ids \"reading\" and \"reading.0\"\n // that never collide — while everything reading the entry prefix (the\n // pager's stimulus grouping, for one) treats them as the same entry, and\n // shows the group's passage above the unrelated loose question. Positional\n // ids could not express this: an index is a loose item or a group, never\n // both.\n const seenEntryKeys = new Set<string>();\n for (const slot of slots) {\n if (seenSlotIds.has(slot.slotId)) {\n throw new Error(\n `flattenSequence: duplicate slot id \"${slot.slotId}\". Two entries declare the same ` +\n 'slotKey, so responses stored against them could not be told apart.',\n );\n }\n seenSlotIds.add(slot.slotId);\n }\n for (const { entry, entryIndex } of ordered) {\n const entryKey = keyOf(entry) ?? String(entryIndex);\n if (seenEntryKeys.has(entryKey)) {\n throw new Error(\n `flattenSequence: duplicate entry key \"${entryKey}\". A loose activity and an item group ` +\n 'cannot share a slotKey — their slot ids would not collide, but everything that reads ' +\n 'the entry they belong to would treat them as one entry.',\n );\n }\n seenEntryKeys.add(entryKey);\n }\n return slots;\n}\n","import type { ActivityMedia, NativeControlHint } from './types/activity.js';\nimport type { AttemptPlan } from './types/attempt-plan.js';\nimport type { MediaPlayLedger, MediaPlayLedgerEntry } from './types/media-budget.js';\n\nexport type {\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedger,\n MediaPlayLedgerEntry,\n} from './types/media-budget.js';\n\n/**\n * The authored-entry prefix of a slot id: `\"3\"` for both `\"3\"` and `\"3.1\"`.\n *\n * Safe because every slot id in a plan is produced by `flattenSequence`, and\n * `keyOf` rejects a `.` in both an entry key and an item key — so the first\n * `.` is always the group/item separator.\n */\nexport function entryKeyOf(slotId: string): string {\n const dot = slotId.indexOf('.');\n return dot === -1 ? slotId : slotId.slice(0, dot);\n}\n\n/** Budget key for an activity's OWN `data.media`. One budget per slot. */\nexport function slotMediaKey(slotId: string): string {\n return `slot:${slotId}`;\n}\n\n/**\n * Budget key for an item group's `stimulus.media`.\n *\n * Keyed by the ENTRY, not the slot: one recording serves every question in the\n * group, so `\"3.0\"`…`\"3.5\"` share one budget. Keying by `slotId` would hand a\n * six-question `maxPlays: 2` listening group twelve plays; keying by\n * `stimulus.id` would collide across papers, which is the thing slot keys and\n * `planHash` exist to prevent.\n */\nexport function stimulusMediaKey(slotId: string): string {\n return `stimulus:${entryKeyOf(slotId)}`;\n}\n\n/** A playback policy with every default resolved. One derivation, used everywhere. */\nexport interface ResolvedPlaybackPolicy {\n controls: 'native' | 'minimal';\n /** `null` when the recording is unbudgeted. */\n maxPlays: number | null;\n seek: 'allow' | 'none';\n rate: 'allow' | 'fixed';\n nativeControlHints: readonly NativeControlHint[];\n}\n\n/**\n * Resolves the defaults once, so the schema refinements, the plan and the\n * renderer cannot drift apart about what a policy means.\n *\n * Absent `playback` resolves to exactly the SDK's pre-0.8.0 behaviour:\n * `controls: 'native'`, no budget, free seeking, free speed, no hints.\n */\nexport function resolvePlaybackPolicy(media: ActivityMedia): ResolvedPlaybackPolicy {\n const p = media.playback;\n const budgeted = typeof p?.maxPlays === 'number';\n const seek = p?.seek ?? (budgeted ? 'none' : 'allow');\n const rate = p?.rate ?? 'allow';\n const controls =\n p?.controls ?? (budgeted || seek === 'none' || rate === 'fixed' ? 'minimal' : 'native');\n return {\n controls,\n maxPlays: budgeted ? (p?.maxPlays as number) : null,\n seek,\n rate,\n nativeControlHints: p?.nativeControlHints ?? [],\n };\n}\n\n/**\n * Every budgeted recording a plan contains: budget key → the `maxPlays` that\n * was frozen when the attempt was planned.\n *\n * Use it to build the storage rows an attempt needs, and to check a stored\n * ledger against the paper it claims to belong to.\n */\nexport function planMediaBudgets(plan: AttemptPlan): Record<string, number> {\n const out: Record<string, number> = {};\n for (const slot of plan.slots) {\n for (const budget of slot.mediaBudgets ?? []) {\n out[budget.key] = budget.maxPlays;\n }\n }\n return out;\n}\n\nfunction assertEntry(key: string, entry: MediaPlayLedgerEntry): void {\n if (\n entry === null ||\n typeof entry !== 'object' ||\n !Number.isInteger(entry.plays) ||\n entry.plays < 0\n ) {\n throw new Error(\n `serializeMediaPlayLedger: play count for ${JSON.stringify(key)} is ` +\n `${JSON.stringify((entry as MediaPlayLedgerEntry | null)?.plays)}. A count must be a ` +\n 'non-negative integer; Number(a NULL column) is the usual cause.',\n );\n }\n if (entry.at !== undefined && (!Number.isFinite(entry.at) || entry.at < 0)) {\n throw new Error(\n `serializeMediaPlayLedger: position for ${JSON.stringify(key)} is ` +\n `${JSON.stringify(entry.at)}. A position must be a finite number of seconds >= 0.`,\n );\n }\n}\n\n/**\n * Validates a set of spent-play counts against the plan that budgeted them,\n * and stamps the envelope.\n *\n * Validates on the way **in**: a play recorded against a recording the paper\n * does not budget is a bug at the moment it is written, and discovering it\n * when a learner tries to resume is discovering it far too late.\n *\n * @throws Error when a key is not budgeted by the plan, or a count is not a\n * non-negative integer, or a position is not a finite number.\n */\nexport function serializeMediaPlayLedger(\n plan: AttemptPlan,\n entries: Readonly<Record<string, MediaPlayLedgerEntry>>,\n options: { savedAt?: string } = {},\n): MediaPlayLedger {\n if (entries === null || typeof entries !== 'object') {\n throw new Error('serializeMediaPlayLedger: entries must be an object keyed by media key.');\n }\n const budgets = planMediaBudgets(plan);\n const out: Record<string, MediaPlayLedgerEntry> = {};\n\n for (const key of Object.keys(entries)) {\n if (!Object.hasOwn(budgets, key)) {\n throw new Error(\n `serializeMediaPlayLedger: media play recorded against a recording the plan does not ` +\n `budget: ${JSON.stringify(key)}. Keys come from slotMediaKey() / stimulusMediaKey(); a ` +\n 'key that belongs to no budgeted media means the client and the plan disagree about ' +\n 'which paper this is.',\n );\n }\n const entry = entries[key] as MediaPlayLedgerEntry;\n assertEntry(key, entry);\n out[key] = { plays: entry.plays, ...(entry.at !== undefined ? { at: entry.at } : {}) };\n }\n\n return {\n ledgerVersion: '1.0',\n planHash: plan.planHash,\n entries: out,\n ...(options.savedAt !== undefined ? { savedAt: options.savedAt } : {}),\n };\n}\n\n/**\n * Reopens a stored ledger against the paper it was spent on.\n *\n * Refuses a ledger from a different paper for the same reason\n * `restoreAttemptState` does: budget keys are short and repeat across papers\n * (`slot:0`, `stimulus:1`), so a ledger from last term's midterm would hold a\n * learner to a budget from an exam they never sat, and look entirely plausible\n * doing it.\n *\n * @throws Error on an unrecognised `ledgerVersion`, a `planHash` mismatch, or\n * anything `serializeMediaPlayLedger` refuses.\n */\nexport function restoreMediaPlayLedger(\n plan: AttemptPlan,\n stored: MediaPlayLedger,\n): MediaPlayLedger {\n if (stored === null || typeof stored !== 'object') {\n throw new Error('restoreMediaPlayLedger: the stored ledger is not an object.');\n }\n if (stored.ledgerVersion !== '1.0') {\n throw new Error(\n `restoreMediaPlayLedger: unsupported ledgerVersion ${JSON.stringify(stored.ledgerVersion)}. ` +\n 'This build understands \"1.0\"; a newer ledger must be migrated before it is restored.',\n );\n }\n if (stored.entries === null || typeof stored.entries !== 'object') {\n throw new Error(\n 'restoreMediaPlayLedger: the ledger carries no entries object (a NULL column, or a ' +\n 'partially written row).',\n );\n }\n if (stored.planHash !== plan.planHash) {\n throw new Error(\n 'restoreMediaPlayLedger: this ledger belongs to a different paper ' +\n `(ledger ${stored.planHash}, plan ${plan.planHash}). Restoring it would hold the learner ` +\n 'to a play budget from an exam they never sat.',\n );\n }\n return serializeMediaPlayLedger(plan, stored.entries, {\n ...(stored.savedAt !== undefined ? { savedAt: stored.savedAt } : {}),\n });\n}\n","import { contentHash } from './content-hash.js';\nimport { resolveDeliveryPolicy, validateDeliveryPolicy } from './delivery.js';\nimport { flattenSequence } from './item-group.js';\nimport { resolvePlaybackPolicy, slotMediaKey, stimulusMediaKey } from './media-budget.js';\nimport type { ScoredItem } from './scoring/compose.js';\nimport { resolveItemScoringPolicy, validateItemScoringPolicy } from './scoring/item-scoring.js';\nimport type { ActivityData, ActivityMedia, ItemOutcome } from './types/activity.js';\nimport type {\n AttemptPlan,\n AttemptPlanDrift,\n AttemptPlanSlot,\n MediaBudgetRef,\n} from './types/attempt-plan.js';\nimport type { DeliveryPolicy, ResolvedDeliveryPolicy } from './types/delivery.js';\nimport type { SequenceEntry, SequenceSlot } from './types/item-group.js';\nimport type { ItemScoringPolicy, ResolvedItemScoringPolicy } from './types/item-scoring.js';\n\nexport type {\n AttemptPlan,\n AttemptPlanDrift,\n AttemptPlanSlot,\n MediaBudgetRef,\n} from './types/attempt-plan.js';\n\n/** Options for {@link planAttempt}. */\nexport interface PlanAttemptOptions<TItem> {\n /** Shuffle the top-level entries. A group moves as one block. */\n shuffleEntries?: boolean;\n /**\n * Seed for every shuffle in this attempt. Required whenever anything\n * shuffles — the attempt has to be reproducible, which is the entire point\n * of writing a plan down. Use the attempt id.\n */\n seed?: string;\n /**\n * What each slot is worth. Defaults to 1 for every slot.\n *\n * Points are resolved HERE, not read from content, because they belong to\n * the paper rather than the item: the same question is worth 1 in a practice\n * quiz and 3 in a final. Whatever this returns is frozen into the plan and\n * is what `composeAssessmentScore` will weight by.\n *\n * @throws Error when it returns a value that is not a finite, non-negative\n * number — a slot worth `NaN` points would poison the whole total.\n */\n points?: (slot: SequenceSlot<TItem>) => number;\n /**\n * The delivery policy this attempt is sat under — whether hints were\n * offered, whether AI help was, whether feedback and solutions showed.\n * Frozen into the plan with every setting spelled out, so the record of the\n * conditions a grade was earned under does not depend on today's defaults.\n *\n * Recorded only when given: a plan made without one is byte-identical to a\n * plan made before policies existed, `planHash` included.\n *\n * @throws Error when the policy does not pass `validateDeliveryPolicy` —\n * a misspelled restriction is no restriction, and a plan is the\n * record an appeal reads.\n */\n delivery?: DeliveryPolicy | null;\n /**\n * The scoring policy this attempt is sat under — how many tries each\n * question gives, which one counts, and what tries and hints cost. Frozen\n * into the plan with every setting spelled out, like `delivery`, and for a\n * stronger reason: every setting in it moves a grade, so a re-grade must\n * apply the rules the learner sat under, not today's.\n *\n * Recorded only when given: a plan made without one is byte-identical to a\n * plan made before scoring policies existed, `planHash` included.\n *\n * @throws Error when the policy does not pass `validateItemScoringPolicy`.\n */\n scoring?: ItemScoringPolicy | null;\n}\n\n/**\n * The activity as CONTENT, with its slot key removed.\n *\n * `slotKey` rides on the same object but is identity, not content — the whole\n * point of the design. Including it in the fingerprint meant that annotating\n * an existing entry with the key that pins its identity reported as\n * \"this question was edited\", which is precisely backwards.\n */\nfunction contentOf(activity: object): object {\n if (!Object.hasOwn(activity, 'slotKey')) {\n return activity;\n }\n const { slotKey: _slotKey, ...content } = activity as { slotKey?: unknown };\n return content;\n}\n\n/**\n * The budgeted recordings a slot presents: its own media, and its group's\n * stimulus. Returns `undefined` — not `[]` — when nothing is budgeted, so the\n * conditional spread below leaves the serialized slot byte-identical to what\n * every pre-0.8.0 paper produced.\n */\nfunction mediaBudgetsOf<TItem extends { id: string; type: string }>(\n slot: SequenceSlot<TItem>,\n): MediaBudgetRef[] | undefined {\n const budgets: MediaBudgetRef[] = [];\n // Read structurally: `media` is a shared optional field, not part of the\n // `{ id, type }` bound this function is generic over.\n const own = (slot.activity as { media?: ActivityMedia }).media;\n if (own !== undefined) {\n const maxPlays = resolvePlaybackPolicy(own).maxPlays;\n if (maxPlays !== null) {\n budgets.push({ key: slotMediaKey(slot.slotId), maxPlays });\n }\n }\n const stimulusMedia = slot.group?.stimulus.media;\n if (stimulusMedia !== undefined) {\n const maxPlays = resolvePlaybackPolicy(stimulusMedia).maxPlays;\n if (maxPlays !== null) {\n budgets.push({ key: stimulusMediaKey(slot.slotId), maxPlays });\n }\n }\n return budgets.length > 0 ? budgets : undefined;\n}\n\n/**\n * Refuses a paper in which one recording is budgeted under several keys.\n *\n * Six questions that each carry the same `/audio/part2.mp3` with `maxPlays: 2`\n * are six budgets, so the learner gets twelve plays of one recording while the\n * paper says two. The fix is authoring, not arithmetic: questions that share a\n * recording belong in an item group, whose stimulus is one recording with one\n * budget.\n */\nfunction assertNoSplitBudgets<TItem extends { id: string; type: string }>(\n slots: readonly SequenceSlot<TItem>[],\n): void {\n const keysByUrl = new Map<string, string[]>();\n for (const slot of slots) {\n const own = (slot.activity as { media?: ActivityMedia }).media;\n if (own === undefined || resolvePlaybackPolicy(own).maxPlays === null) {\n continue;\n }\n const keys = keysByUrl.get(own.url) ?? [];\n keys.push(slotMediaKey(slot.slotId));\n keysByUrl.set(own.url, keys);\n }\n for (const [url, keys] of keysByUrl) {\n if (keys.length > 1) {\n throw new Error(\n `planAttempt: media ${JSON.stringify(url)} is budgeted under ${keys.length} separate ` +\n `keys (${keys.join(', ')}), so one recording grants ${keys.length} × maxPlays. Put the ` +\n \"questions that share a recording in an item group — a group's stimulus is one \" +\n 'recording with one budget.',\n );\n }\n }\n}\n\nfunction planSlot<TItem extends { id: string; type: string }>(\n slot: SequenceSlot<TItem>,\n points: number,\n): AttemptPlanSlot {\n if (!Number.isFinite(points) || points < 0) {\n throw new Error(\n `planAttempt: slot \"${slot.slotId}\" resolved to ${String(points)} points. Points must be a ` +\n 'finite, non-negative number, or the paper has no defensible total.',\n );\n }\n const mediaBudgets = mediaBudgetsOf(slot);\n return {\n slotId: slot.slotId,\n index: slot.index,\n activityId: slot.activity.id,\n activityType: slot.activity.type,\n points,\n contentHash: contentHash(contentOf(slot.activity)),\n ...(slot.group !== undefined\n ? {\n group: {\n id: slot.group.id,\n ...(slot.group.title !== undefined ? { title: slot.group.title } : {}),\n stimulusHash: contentHash(slot.group.stimulus),\n ...(slot.group.cue !== undefined\n ? {\n cue: {\n id: slot.group.cue.id,\n at: slot.group.cue.at,\n ...(slot.group.cue.required !== undefined\n ? { required: slot.group.cue.required }\n : {}),\n },\n }\n : {}),\n },\n }\n : {}),\n ...(mediaBudgets !== undefined ? { mediaBudgets } : {}),\n };\n}\n\n/**\n * Freezes what an attempt is being served: the presented order, each slot's\n * identity and worth, and a fingerprint of the content behind it.\n *\n * Call this once, when the attempt starts, and store the result beside the\n * responses. Everything that decides the grade is then a historical fact\n * rather than a re-read of content that may since have changed — which is what\n * makes a re-grade, a remark or an appeal answerable.\n *\n * Ordering comes from `flattenSequence`, so a plan and a live render of the\n * same entries with the same seed agree slot for slot.\n *\n * @throws Error when a shuffle is requested without a seed, when a group is\n * empty, when two entries collide on a `slotKey`, or when `points`\n * returns a value that is not finite and non-negative.\n */\nexport function planAttempt<TItem extends { id: string; type: string }>(\n entries: readonly SequenceEntry<TItem>[],\n options: PlanAttemptOptions<TItem> = {},\n): AttemptPlan {\n const { seed, shuffleEntries, points } = options;\n const delivery = frozenDelivery(options.delivery);\n const scoring = frozenScoring(options.scoring);\n const slots = flattenSequence(entries, {\n ...(shuffleEntries !== undefined ? { shuffleEntries } : {}),\n ...(seed !== undefined ? { seed } : {}),\n });\n\n assertNoSplitBudgets(slots);\n\n const planned = slots.map((slot) => planSlot(slot, points?.(slot) ?? 1));\n const totalPoints = planned.reduce((sum, slot) => sum + slot.points, 0);\n\n return {\n planVersion: '1.0',\n ...(seed !== undefined ? { seed } : {}),\n ...(shuffleEntries !== undefined ? { shuffleEntries } : {}),\n // The plan's own fingerprint covers the slots verbatim — identity, order,\n // points and content hashes — so one stored value answers \"is this still\n // the paper that was sat?\" and the per-slot hashes then say what moved.\n // A policy joins it when there is one: the same paper sat with hints and\n // without is two different attempts. Without one, the fingerprint is the\n // one every plan before policies had.\n // So does a scoring policy: the same answers are worth something else when\n // a hint costs a tenth of the marks. Each joins only when it is there, so\n // a plan with neither — or with a delivery policy alone — keeps the\n // fingerprint it always had.\n planHash: contentHash(\n delivery === undefined && scoring === undefined\n ? planned\n : {\n slots: planned,\n ...(delivery !== undefined ? { delivery } : {}),\n ...(scoring !== undefined ? { scoring } : {}),\n },\n ),\n ...(delivery !== undefined ? { delivery } : {}),\n ...(scoring !== undefined ? { scoring } : {}),\n slots: planned,\n totalPoints,\n };\n}\n\n/**\n * Compares a stored plan with the content as it stands now, and reports every\n * way they have drifted apart.\n *\n * This is the question a remark or an appeal actually asks: *is the paper I am\n * looking at the paper this learner sat?* Ids alone cannot answer it, because\n * they survive an edit unchanged.\n *\n * Rebuild the comparison plan with the SAME options the stored one recorded —\n * its `seed`, its `shuffleEntries`, and the same `points` function — or the\n * differences you see will be your own:\n *\n * ```ts\n * const now = planAttempt(currentEntries, {\n * seed: stored.seed,\n * shuffleEntries: stored.shuffleEntries,\n * points: pointsFor, // omit it and every slot reweights to 1\n * });\n * const drift = verifyAttemptPlan(stored, now);\n * ```\n *\n * A drift is not automatically a problem — a fixed typo changes a hash without\n * changing what was asked. It is a fact somebody has to be able to see.\n */\nexport function verifyAttemptPlan(plan: AttemptPlan, current: AttemptPlan): AttemptPlanDrift {\n const before = new Map(plan.slots.map((slot) => [slot.slotId, slot]));\n const after = new Map(current.slots.map((slot) => [slot.slotId, slot]));\n\n const missingSlotIds = plan.slots.filter((s) => !after.has(s.slotId)).map((s) => s.slotId);\n const addedSlotIds = current.slots.filter((s) => !before.has(s.slotId)).map((s) => s.slotId);\n\n const changedSlotIds: string[] = [];\n const changedStimulusSlotIds: string[] = [];\n const changedCueSlotIds: string[] = [];\n const changedPointsSlotIds: string[] = [];\n const reorderedSlotIds: string[] = [];\n // Not a slot's: the conditions of the whole attempt. Compared as spelled out,\n // so a policy recorded before and one recorded now differ only where a\n // setting does.\n const deliveryChanged =\n contentHash(plan.delivery ?? null) !== contentHash(current.delivery ?? null);\n const scoringChanged = contentHash(plan.scoring ?? null) !== contentHash(current.scoring ?? null);\n for (const slot of plan.slots) {\n const now = after.get(slot.slotId);\n if (now === undefined) {\n continue;\n }\n // A swapped activity changes the content behind the slot, so it lands in\n // `changedSlotIds` through the fingerprint rather than needing its own arm.\n if (now.contentHash !== slot.contentHash) {\n changedSlotIds.push(slot.slotId);\n }\n if (now.group?.stimulusHash !== slot.group?.stimulusHash) {\n changedStimulusSlotIds.push(slot.slotId);\n }\n if (contentHash(now.group?.cue ?? null) !== contentHash(slot.group?.cue ?? null)) {\n changedCueSlotIds.push(slot.slotId);\n }\n // A reweight moves the grade without touching a single question. Left out,\n // `matches` said the paper was unchanged while its `planHash` disagreed.\n if (now.points !== slot.points) {\n changedPointsSlotIds.push(slot.slotId);\n }\n if (now.index !== slot.index) {\n reorderedSlotIds.push(slot.slotId);\n }\n }\n\n return {\n matches:\n missingSlotIds.length === 0 &&\n addedSlotIds.length === 0 &&\n changedSlotIds.length === 0 &&\n changedStimulusSlotIds.length === 0 &&\n changedCueSlotIds.length === 0 &&\n changedPointsSlotIds.length === 0 &&\n reorderedSlotIds.length === 0 &&\n !deliveryChanged &&\n !scoringChanged,\n missingSlotIds,\n addedSlotIds,\n changedSlotIds,\n changedStimulusSlotIds,\n changedCueSlotIds,\n changedPointsSlotIds,\n reorderedSlotIds,\n // Present only when it is true, as `rejectedSlotIds` is on a composed\n // score: a drift report between two plans without a policy — every one\n // written before policies existed — keeps exactly the shape it had.\n ...(deliveryChanged ? { deliveryChanged: true as const } : {}),\n ...(scoringChanged ? { scoringChanged: true as const } : {}),\n };\n}\n\n/**\n * The policy a plan records: checked, then spelled out. `undefined` when there\n * is none, so the plan carries no field at all rather than an empty one.\n */\nfunction frozenDelivery(policy: unknown): ResolvedDeliveryPolicy | undefined {\n if (policy === undefined || policy === null) {\n return undefined;\n }\n const checked = validateDeliveryPolicy(policy);\n if (!checked.success) {\n const first = checked.issues[0];\n throw new Error(\n `planAttempt: the delivery policy is not one to record — ${first?.message ?? 'invalid'}` +\n (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ''),\n );\n }\n return resolveDeliveryPolicy(checked.data);\n}\n\n/** The scoring policy a plan records, as {@link frozenDelivery} records a delivery policy. */\nfunction frozenScoring(policy: unknown): ResolvedItemScoringPolicy | undefined {\n if (policy === undefined || policy === null) {\n return undefined;\n }\n const checked = validateItemScoringPolicy(policy);\n if (!checked.success) {\n const first = checked.issues[0];\n throw new Error(\n `planAttempt: the scoring policy is not one to record — ${first?.message ?? 'invalid'}` +\n (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ''),\n );\n }\n return resolveItemScoringPolicy(checked.data);\n}\n\n/** How {@link scoredItemsFromPlan} fills a slot that has no recorded outcome. */\nexport type MissingOutcomePolicy =\n /**\n * Default. `{ status: 'deferred', reason: 'no_response_recorded' }` — a\n * non-terminal state, so the composed result stays `provisional` and cannot\n * be recorded as a final pass or fail.\n */\n | 'deferred'\n /**\n * `{ status: 'scored', score: 0 }` — the learner left it blank on a paper\n * that IS complete. Choose this only when you know the attempt was\n * submitted; it is a real zero and makes the result final.\n */\n | 'zero'\n /** Build the outcome yourself, per slot. */\n | ((slot: AttemptPlanSlot) => ItemOutcome);\n\nfunction missingOutcome(slot: AttemptPlanSlot, policy: MissingOutcomePolicy): ItemOutcome {\n if (typeof policy === 'function') {\n return policy(slot);\n }\n if (policy === 'zero') {\n return { status: 'scored', score: 0, maxScore: 1, passed: false, feedback: null, details: [] };\n }\n return { status: 'deferred', reason: 'no_response_recorded', maxScore: 1 };\n}\n\n/**\n * Turns a plan plus whatever outcomes exist into the `ScoredItem[]`\n * `composeAssessmentScore` consumes.\n *\n * The plan is the source of the denominator, not the outcomes. A slot the\n * learner never reached still has to appear — otherwise it silently leaves the\n * denominator and the remaining questions quietly become worth more than the\n * paper says.\n *\n * **A missing outcome defaults to `deferred`, never `unscorable`.** The\n * distinction decides a grade: `unscorable` means \"a grade is never coming\",\n * so `composeAssessmentScore` drops the slot from the denominator AND lets the\n * result go `final` — which turned a three-question paper with one answer into\n * a final, passing 100%. `deferred` means \"not yet\", which holds the result\n * `provisional` so nothing can be recorded. Pass `'zero'` once you know the\n * attempt was submitted and the blanks are genuinely blanks.\n *\n * Pass outcomes keyed by `slotId`. Keys the plan does not know are ignored:\n * a plan is the authority on what the attempt contained.\n */\nexport function scoredItemsFromPlan(\n plan: AttemptPlan,\n outcomes: Readonly<Record<string, ItemOutcome>>,\n options: { missing?: MissingOutcomePolicy } = {},\n): ScoredItem[] {\n const policy = options.missing ?? 'deferred';\n return plan.slots.map((slot) => ({\n slotId: slot.slotId,\n activityId: slot.activityId,\n points: slot.points,\n // `Object.hasOwn`, not a bare lookup: slot ids come from authored\n // `slotKey`s, and a key of `constructor` or `toString` would otherwise\n // resolve through the prototype chain and hand a FUNCTION to the scorer\n // in place of an outcome.\n outcome: Object.hasOwn(outcomes, slot.slotId)\n ? (outcomes[slot.slotId] as ItemOutcome)\n : missingOutcome(slot, policy),\n }));\n}\n\n/** Convenience alias: the entry type a plan is built from. */\nexport type PlannableEntry = SequenceEntry<ActivityData>;\n","import { canonicalJson } from './content-hash.js';\nimport type { LearnerResponse } from './types/activity.js';\nimport type { AttemptPlan } from './types/attempt-plan.js';\nimport type { AttemptState, ResponseDiffEntry } from './types/attempt-state.js';\n\nexport type { AttemptState, ResponseDiffEntry } from './types/attempt-state.js';\n\n/** What {@link serializeAttemptState} is given about an attempt in progress. */\nexport interface AttemptProgress {\n /** Answers so far, keyed by `slotId`. */\n responses: Readonly<Record<string, LearnerResponse>>;\n /** Slots already submitted. Defaults to none. */\n submittedSlotIds?: readonly string[];\n /** Presented position the learner is on. Defaults to 0. */\n index?: number;\n /** ISO 8601 timestamp to stamp the snapshot with. The SDK does not read a clock. */\n savedAt?: string;\n}\n\n/**\n * A DEEP copy of the responses.\n *\n * A one-level spread is not enough: every `LearnerResponse` shape is itself an\n * object (`selectedOptionIds`, `answers`, `text`), so a spread hands back a new\n * map of the caller's SAME response objects. A consumer whose reducer updates\n * an answer in place — an Immer draft, a push onto a multi-select, or\n * `answers[blankId] = text`, which is the natural update for that shape — then\n * mutates every snapshot ever taken. The stored snapshot retroactively becomes\n * the new answer, `diffResponses` sees no change, and a delta autosave writes\n * nothing for an edit the learner really made.\n */\nfunction cloneResponses(\n responses: Readonly<Record<string, LearnerResponse>>,\n): Record<string, LearnerResponse> {\n // Responses are JSON by contract, so the fallback is lossless for them.\n return typeof structuredClone === 'function'\n ? structuredClone(responses as Record<string, LearnerResponse>)\n : (JSON.parse(JSON.stringify(responses)) as Record<string, LearnerResponse>);\n}\n\n/** Slot ids present in the object but unknown to the plan. */\nfunction unknownKeys(plan: AttemptPlan, keys: readonly string[]): string[] {\n const known = new Set(plan.slots.map((slot) => slot.slotId));\n return keys.filter((key) => !known.has(key));\n}\n\n/**\n * Captures an in-progress attempt as a storable snapshot, bound to its plan.\n *\n * Validated against the plan on the way in, not on the way out: a response\n * stored under a slot the paper does not contain is a bug at the moment it is\n * written, and finding it months later — when a learner tries to resume — is\n * finding it far too late.\n *\n * The SDK reads no clock: pass `savedAt` if you want the snapshot stamped, so\n * the function stays pure and its output stays reproducible in a test.\n *\n * @throws Error when a response or submitted slot is not in the plan, or when\n * `index` is not a position the plan actually has.\n */\nexport function serializeAttemptState(plan: AttemptPlan, progress: AttemptProgress): AttemptState {\n const responseKeys = Object.keys(progress.responses);\n const submittedSlotIds = [...(progress.submittedSlotIds ?? [])];\n\n const strayResponses = unknownKeys(plan, responseKeys);\n if (strayResponses.length > 0) {\n throw new Error(\n `serializeAttemptState: response recorded for slot(s) the plan does not contain: ${strayResponses.join(', ')}. ` +\n 'A response that belongs to no question cannot be scored and would be lost silently.',\n );\n }\n const straySubmitted = unknownKeys(plan, submittedSlotIds);\n if (straySubmitted.length > 0) {\n throw new Error(\n `serializeAttemptState: submitted slot(s) the plan does not contain: ${straySubmitted.join(', ')}.`,\n );\n }\n\n const index = progress.index ?? 0;\n // An out-of-range position would reopen the attempt on a question that is\n // not there — which the pager renders as an empty shell with no way forward.\n // An empty plan admits only 0: guarding with `slots.length > 0` skipped the\n // range check altogether there, so a zero-slot paper accepted any index and\n // the error message contradicted itself.\n const lastIndex = Math.max(0, plan.slots.length - 1);\n if (!Number.isInteger(index) || index < 0 || index > lastIndex) {\n throw new Error(\n `serializeAttemptState: index ${String(index)} is not a position in a ${plan.slots.length}-slot plan.`,\n );\n }\n\n return {\n stateVersion: '1.0',\n planHash: plan.planHash,\n // Copied DEEPLY, not aliased: a snapshot that keeps mutating with the\n // live attempt is not a snapshot. See cloneResponses.\n responses: cloneResponses(progress.responses),\n submittedSlotIds,\n index,\n ...(progress.savedAt !== undefined ? { savedAt: progress.savedAt } : {}),\n };\n}\n\n/**\n * Reopens a stored snapshot against a plan, refusing anything that does not\n * belong to it.\n *\n * The `planHash` check is the point. Slot ids are short and stable by design,\n * so a snapshot from a DIFFERENT paper — last term's midterm, a sibling\n * version, a copy-pasted attempt row — will happily line its answers up\n * against the wrong questions and look entirely plausible doing it. Comparing\n * the paper's fingerprint is what makes that impossible rather than unlikely.\n *\n * @throws Error when the snapshot belongs to a different plan, or references\n * slots the plan does not contain.\n */\nexport function restoreAttemptState(plan: AttemptPlan, state: AttemptState): AttemptState {\n if (state === null || typeof state !== 'object') {\n throw new Error('restoreAttemptState: the snapshot is not an object.');\n }\n // The one field added so a stored snapshot survives an envelope change was\n // the one field never read: an unrecognised version was accepted,\n // reinterpreted under this version's rules, and re-stamped '1.0' — which\n // also destroyed the evidence that it had ever been anything else.\n if (state.stateVersion !== '1.0') {\n throw new Error(\n `restoreAttemptState: unsupported stateVersion ${JSON.stringify(state.stateVersion)}. ` +\n 'This build understands \"1.0\"; a newer snapshot must be migrated before it is restored.',\n );\n }\n if (state.responses === null || typeof state.responses !== 'object') {\n throw new Error(\n 'restoreAttemptState: the snapshot carries no responses object (a NULL column, or a ' +\n 'partially written row).',\n );\n }\n if (state.planHash !== plan.planHash) {\n throw new Error(\n 'restoreAttemptState: this snapshot belongs to a different paper ' +\n `(snapshot ${state.planHash}, plan ${plan.planHash}). Restoring it would attach the ` +\n \"learner's answers to questions they never saw.\",\n );\n }\n // Re-validated rather than trusted: a snapshot is storage, and storage is\n // edited, migrated and hand-fixed.\n return serializeAttemptState(plan, {\n responses: state.responses,\n submittedSlotIds: state.submittedSlotIds,\n index: state.index,\n ...(state.savedAt !== undefined ? { savedAt: state.savedAt } : {}),\n });\n}\n\n/**\n * Reports how two snapshots' responses differ, slot by slot.\n *\n * Useful for an autosave that should only write what moved, and for an audit\n * trail that has to show what a learner changed between two saves — including\n * an answer they cleared, which a naive comparison of the later snapshot\n * alone cannot see.\n *\n * Entries come back sorted by `slotId`, so the output is stable regardless of\n * the order the two objects happened to be written in.\n */\nexport function diffResponses(\n before: Pick<AttemptState, 'responses'>,\n after: Pick<AttemptState, 'responses'>,\n): ResponseDiffEntry[] {\n const slotIds = [...new Set([...Object.keys(before.responses), ...Object.keys(after.responses)])];\n slotIds.sort();\n\n const entries: ResponseDiffEntry[] = [];\n for (const slotId of slotIds) {\n const had = Object.hasOwn(before.responses, slotId);\n const has = Object.hasOwn(after.responses, slotId);\n const from = before.responses[slotId];\n const to = after.responses[slotId];\n\n if (had && !has) {\n entries.push({ slotId, change: 'removed', ...(from !== undefined ? { before: from } : {}) });\n continue;\n }\n if (!had && has) {\n entries.push({ slotId, change: 'added', ...(to !== undefined ? { after: to } : {}) });\n continue;\n }\n // Structural comparison through the canonical form, so a response that\n // survived a JSON round-trip with its keys reordered does not read as a\n // change the learner never made.\n if (canonicalJson(from) !== canonicalJson(to)) {\n entries.push({\n slotId,\n change: 'changed',\n ...(from !== undefined ? { before: from } : {}),\n ...(to !== undefined ? { after: to } : {}),\n });\n }\n }\n return entries;\n}\n","import { ActivitySchemaError, UnknownActivityTypeError } from './errors.js';\nimport { MEDIA_FIELD_POLICY } from './registry/builtins.js';\nimport { getActivityTypeDescriptor } from './registry/index.js';\nimport type { FieldPolicy, Sensitivity } from './registry/registry.js';\nimport { RedactedItemGroupSchema } from './schemas/item-group.js';\nimport { readSchema } from './schemas/read-schema.js';\nimport type { ItemGroup } from './types/item-group.js';\n\n/**\n * A learner-safe projection of activity data produced by {@link redact}:\n * the activity's public fields plus the `redacted: true` marker. Renderable\n * and validatable (each built-in type registers a strict redacted schema),\n * but stripped of the answer key, scoring rules, authored feedback, and\n * author-only assets.\n */\nexport interface RedactedActivityData {\n redacted: true;\n schemaVersion: string;\n type: string;\n id: string;\n title: string;\n [key: string]: unknown;\n}\n\n/** Options for {@link redact}. */\nexport interface RedactOptions {\n /**\n * Which sensitivity tiers to keep beyond `public`:\n * - `'none'` (default) — public fields only; the output satisfies the\n * type's strict redacted schema and `assertRedacted`.\n * - `'after-submit'` — public + answer-key fields (for post-submission\n * review renders). `author-only` fields and unclassified fields are\n * STILL removed; the output will NOT pass `assertRedacted`.\n */\n reveal?: 'none' | 'after-submit';\n /**\n * Per-call sensitivity overrides, merged over the type's registered\n * `fieldPolicy` (top-level keys replace; nested objects merge one level).\n *\n * Sensitivity is partly a PEDAGOGICAL decision, not purely a security one —\n * whether a rubric or a hint is learner-visible differs legitimately between\n * deployments — and the SDK must not freeze that choice. Use this to tighten\n * a field the SDK ships as `public`:\n *\n * ```ts\n * redact(essay, { policy: { rubric: 'author-only' } });\n * ```\n *\n * Overrides can only be applied to fields; they cannot re-open a field the\n * caller has not classified, because unclassified still means removed.\n * What can be tightened is a field the type's `redactedSchema` leaves\n * OPTIONAL, as it leaves the rubric above: the schema check rejects a\n * payload that reveals more than the learner-safe shape, and equally one\n * that is missing part of it. Tightening a field the redacted shape requires\n * — a read-aloud item's `scoring`, `recording`, `referenceText` or `locale`,\n * without which the learner has no item to read — throws\n * {@link ActivitySchemaError} instead of hiding it.\n */\n policy?: FieldPolicy;\n}\n\n/** Merges per-call overrides over a registered policy, one level deep. */\nfunction mergePolicy(base: FieldPolicy, overrides: FieldPolicy | undefined): FieldPolicy {\n if (overrides === undefined) {\n return base;\n }\n const merged: Record<string, Sensitivity | FieldPolicy> = { ...base };\n for (const [key, override] of Object.entries(overrides)) {\n const current = merged[key];\n merged[key] =\n typeof override === 'object' && typeof current === 'object'\n ? { ...current, ...override }\n : override;\n }\n return merged;\n}\n\nfunction isSensitivity(value: Sensitivity | FieldPolicy): value is Sensitivity {\n return typeof value === 'string';\n}\n\nfunction keepField(sensitivity: Sensitivity, reveal: 'none' | 'after-submit'): boolean {\n if (sensitivity === 'public') {\n return true;\n }\n return sensitivity === 'answer-key' && reveal === 'after-submit';\n}\n\n/** An object projection that kept no fields at all. Arrays are never dropped. */\nfunction isEmptyObject(value: unknown): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0\n );\n}\n\nfunction redactValue(\n value: unknown,\n policy: FieldPolicy,\n reveal: 'none' | 'after-submit',\n): unknown {\n if (Array.isArray(value)) {\n return value.map((element) => redactValue(element, policy, reveal));\n }\n if (value === null || typeof value !== 'object') {\n // A nested FieldPolicy cannot classify a primitive's sub-fields; the\n // policy author classified an object shape that is not there. Fail\n // closed: drop it (handled by the caller returning undefined).\n return undefined;\n }\n\n const source = value as Record<string, unknown>;\n const output: Record<string, unknown> = {};\n for (const [key, fieldValue] of Object.entries(source)) {\n if (fieldValue === undefined) {\n continue;\n }\n const classification = policy[key];\n if (classification === undefined) {\n // Fail closed: unclassified fields are never emitted, at any reveal\n // level. Adding a field without classifying it hides it — never leaks it.\n continue;\n }\n if (isSensitivity(classification)) {\n if (keepField(classification, reveal)) {\n output[key] = fieldValue;\n }\n continue;\n }\n const nested = redactValue(fieldValue, classification, reveal);\n // A nested projection that emitted nothing is omitted rather than emitted\n // as `{}`. \"Nothing survived redaction\" and \"the field was absent\" are the\n // same thing to a learner — and without this, giving an all-`answer-key`\n // object a nested policy would turn its absence under `reveal: 'none'`\n // into an empty object, changing every existing projection.\n if (nested !== undefined && !isEmptyObject(nested)) {\n output[key] = nested;\n }\n }\n return output;\n}\n\n/**\n * Produces the learner-safe projection of activity data (R7), driven by the\n * `fieldPolicy` registered for `data.type`. Fail-closed and exhaustive by\n * construction: a field the policy does not classify is removed — including\n * unknown passthrough fields — so a NEW field added by a future SDK or a\n * consumer sidecar can never leak through an out-of-date redactor.\n *\n * With the default `reveal: 'none'`, the output validates against the type's\n * strict redacted schema (verified here; an invalid projection throws\n * {@link ActivitySchemaError} rather than shipping an unproven payload).\n *\n * @throws UnknownActivityTypeError when `data.type` is not registered.\n * @throws Error when the registered descriptor declares no `fieldPolicy`\n * (redaction cannot guess sensitivities).\n */\nexport function redact<T extends { type: string }>(\n data: T,\n options: RedactOptions = {},\n): RedactedActivityData {\n const reveal = options.reveal ?? 'none';\n const descriptor = getActivityTypeDescriptor(data.type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(data.type));\n }\n if (descriptor.fieldPolicy === undefined) {\n throw new Error(\n `Activity type \"${descriptor.type}\" has no fieldPolicy; redact() cannot run fail-closed redaction without one.`,\n );\n }\n\n const effectivePolicy = mergePolicy(descriptor.fieldPolicy, options.policy);\n const projected = redactValue(data, effectivePolicy, reveal) as Record<string, unknown>;\n const result = { ...projected, redacted: true as const } as RedactedActivityData;\n\n if (reveal === 'none' && descriptor.redactedSchema !== undefined) {\n const parsed = readSchema(\n descriptor.redactedSchema,\n result,\n `Activity type \"${descriptor.type}\"`,\n );\n if (!parsed.success) {\n throw new ActivitySchemaError(\n descriptor.type,\n parsed.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n }\n\n return result;\n}\n\n/**\n * Asserts that `data` is a learner-safe redacted projection: it carries the\n * `redacted: true` marker and, when its type registers a strict redacted\n * schema, validates against it (proving the absence of answer-key fields).\n * Use this at the server boundary before sending activity data to a client\n * that must not hold the key.\n */\nexport function assertRedacted(data: unknown): asserts data is RedactedActivityData {\n if (typeof data !== 'object' || data === null) {\n throw new ActivitySchemaError('unknown', [\n { path: [], message: 'Redacted activity data must be an object.', code: 'invalid_type' },\n ]);\n }\n const candidate = data as Record<string, unknown>;\n if (candidate.redacted !== true) {\n throw new ActivitySchemaError(String(candidate.type ?? 'unknown'), [\n {\n path: ['redacted'],\n message: 'Missing redacted marker — this payload is not a redact() projection.',\n code: 'custom',\n },\n ]);\n }\n const type = typeof candidate.type === 'string' ? candidate.type : '';\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(type);\n }\n if (descriptor.redactedSchema === undefined) {\n // Fail closed: without a registered redacted schema there is no way to\n // PROVE the absence of answer-key fields, and a marker alone proves\n // nothing (any object can carry `redacted: true`).\n throw new ActivitySchemaError(descriptor.type, [\n {\n path: [],\n message: `Activity type \"${descriptor.type}\" registers no redactedSchema; assertRedacted cannot prove this payload is learner-safe.`,\n code: 'custom',\n },\n ]);\n }\n const parsed = readSchema(\n descriptor.redactedSchema,\n candidate,\n `Activity type \"${descriptor.type}\"`,\n );\n if (!parsed.success) {\n throw new ActivitySchemaError(\n descriptor.type,\n parsed.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n}\n\n/**\n * Sensitivity of a stimulus. Everything is learner-visible — that is what a\n * stimulus IS — except the author transcript. Fail-closed like every other\n * policy: a field added to `Stimulus` without a classification here is\n * dropped, never leaked.\n */\nconst STIMULUS_FIELD_POLICY: FieldPolicy = {\n id: 'public',\n kind: 'public',\n title: 'public',\n body: 'public',\n bodyHtml: 'public',\n media: MEDIA_FIELD_POLICY,\n locale: 'public',\n attribution: 'public',\n transcript: 'author-only',\n};\n\n/** The group container's own fields. `items` is handled separately, per item type. */\nconst ITEM_GROUP_FIELD_POLICY: FieldPolicy = {\n schemaVersion: 'public',\n type: 'public',\n id: 'public',\n title: 'public',\n // See the note on the built-in activity policies: a slot key is identity,\n // and identity has to cross the redaction boundary intact.\n slotKey: 'public',\n shuffle: 'public',\n stimulus: STIMULUS_FIELD_POLICY,\n // Public, and classified key by key rather than as one leaf: the learner's\n // player needs every part of it, none of it is a key — and a leaf would hand\n // over the author's object by reference, with whatever else was parked in it.\n timeline: {\n // Applies to every element of the array.\n cues: {\n id: 'public',\n at: 'public',\n itemIds: 'public',\n title: 'public',\n required: 'public',\n },\n chapters: {\n at: 'public',\n title: 'public',\n },\n navigation: 'public',\n },\n};\n\n/** A learner-safe item group: the container with its marker, holding `redact()` projections. */\nexport type RedactedItemGroup = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Produces the learner-safe projection of an item group: the container and\n * stimulus under their own fail-closed policy (the author transcript goes;\n * the passage, media and attribution stay — the learner is meant to see\n * them), and every item through {@link redact} with the same `options`, so\n * a per-call `policy` tightens each item exactly as it would alone.\n *\n * With the default `reveal: 'none'` the container is verified against the\n * strict redacted schema; each item was already verified by `redact`.\n */\nexport function redactItemGroup<TItem extends { type: string }>(\n group: ItemGroup<TItem>,\n options: RedactOptions = {},\n): RedactedItemGroup {\n const reveal = options.reveal ?? 'none';\n const { items, ...container } = group;\n const projected = redactValue(container, ITEM_GROUP_FIELD_POLICY, reveal) as Record<\n string,\n unknown\n >;\n const result = {\n ...projected,\n items: items.map((item) => redact(item, options)),\n redacted: true as const,\n } as RedactedItemGroup;\n\n if (reveal === 'none') {\n const parsed = RedactedItemGroupSchema.safeParse(result);\n if (!parsed.success) {\n throw new ActivitySchemaError(\n 'item-group',\n parsed.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n }\n return result;\n}\n\n/**\n * Asserts that `data` is a learner-safe item group: it carries the marker,\n * its container and stimulus satisfy the strict redacted schema (so no\n * transcript, no unclassified field), and EVERY item passes\n * {@link assertRedacted} against its own type's redacted schema. Item\n * failures are reported at `items.<index>.…`. Use it at the server boundary\n * before sending a group to a client that must not hold the key.\n */\nexport function assertRedactedItemGroup(data: unknown): asserts data is RedactedItemGroup {\n if (typeof data !== 'object' || data === null) {\n throw new ActivitySchemaError('item-group', [\n { path: [], message: 'Redacted item group must be an object.', code: 'invalid_type' },\n ]);\n }\n const candidate = data as Record<string, unknown>;\n if (candidate.redacted !== true) {\n throw new ActivitySchemaError('item-group', [\n {\n path: ['redacted'],\n message: 'Missing redacted marker — this payload is not a redactItemGroup() projection.',\n code: 'custom',\n },\n ]);\n }\n const parsed = RedactedItemGroupSchema.safeParse(candidate);\n if (!parsed.success) {\n throw new ActivitySchemaError(\n 'item-group',\n parsed.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n parsed.data.items.forEach((item, index) => {\n try {\n assertRedacted(item);\n } catch (error) {\n if (error instanceof ActivitySchemaError) {\n throw new ActivitySchemaError(\n 'item-group',\n error.errors.map((issue) => ({\n ...issue,\n path: ['items', String(index), ...issue.path],\n })),\n );\n }\n throw error;\n }\n });\n}\n","import { entryKeyOf } from './media-budget.js';\nimport {\n type AssessmentScore,\n type CompositionPolicy,\n composeAssessmentScore,\n type ScoredItem,\n} from './scoring/compose.js';\nimport type { MediaProgress } from './types/item-group.js';\n\n/**\n * Reads a stored {@link MediaProgress}. Never throws: anything that is not one\n * — a string, a stale shape, an object whose getters throw — answers `null`,\n * and the player starts from the beginning.\n *\n * What it keeps, it clamps. A stored position is host storage, which is input\n * from a stranger by the time it is read back: a negative `at`, a `furthest`\n * behind `at`, or a position past the end of a video that has since been\n * re-cut must not put the playhead somewhere it cannot be. Give `duration`\n * once the video's metadata is known, and both positions are held inside it.\n */\nexport function readMediaProgress(value: unknown, duration?: number): MediaProgress | null {\n let version: unknown;\n let at: unknown;\n let furthest: unknown;\n try {\n if (typeof value !== 'object' || value === null) {\n return null;\n }\n // Each field read once: an accessor that answered the check one value and\n // the clamp another would put back exactly what the check refused.\n ({ progressVersion: version, at, furthest } = value as Record<string, unknown>);\n } catch {\n return null;\n }\n if (version !== '1.0' || typeof at !== 'number' || !Number.isFinite(at)) {\n return null;\n }\n const end =\n typeof duration === 'number' && Number.isFinite(duration) && duration >= 0\n ? duration\n : Number.POSITIVE_INFINITY;\n const clampedAt = Math.min(Math.max(0, at), end);\n const reached = typeof furthest === 'number' && Number.isFinite(furthest) ? furthest : clampedAt;\n return {\n progressVersion: '1.0',\n at: clampedAt,\n furthest: Math.min(Math.max(clampedAt, reached), end),\n };\n}\n\n/**\n * One interactive video's composed result: the scored items whose slot belongs\n * to the entry, composed as one section of weight 1. A slot belongs when\n * `entryKeyOf(item.slotId) === entryKey`, so a host can pass a whole attempt's\n * items and get the video's share of it.\n *\n * `entryKey` is the group's `slotKey` when it has one — which every published\n * video should — or its position in the sequence otherwise. A group object\n * alone cannot know its position, which is why this takes the key.\n *\n * @throws Error when no item belongs to the entry. A mistyped key would\n * otherwise compose nothing into a `final` result, and a refusal must\n * never read as a grade.\n */\nexport function composeTimelineScore(\n entryKey: string,\n items: readonly ScoredItem[],\n policy: CompositionPolicy,\n): AssessmentScore {\n const own = items.filter((item) => entryKeyOf(item.slotId) === entryKey);\n if (own.length === 0) {\n throw new Error(\n `composeTimelineScore: no scored item belongs to entry \"${entryKey}\". Pass the video's ` +\n 'slotKey (or its position in the sequence), and the items of the attempt that holds it.',\n );\n }\n return composeAssessmentScore([{ id: entryKey, weight: 1, items: own }], policy);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/delivery.ts","../src/item-group.ts","../src/media-budget.ts","../src/attempt-plan.ts","../src/attempt-state.ts","../src/redact.ts","../src/timeline.ts"],"sourcesContent":["import type {\n DeliveryPolicy,\n DeliveryPolicyIssue,\n ResolvedDeliveryPolicy,\n} from './types/delivery.js';\n\n/**\n * The policy that restricts nothing: every setting `true`. It is what an absent\n * policy resolves to, and it is exactly the behaviour the SDK had before\n * delivery policies existed.\n */\nexport const OPEN_DELIVERY_POLICY: ResolvedDeliveryPolicy = Object.freeze({\n feedback: true,\n solutions: true,\n hints: true,\n ai: Object.freeze({ hints: true, explanations: true }),\n}) as ResolvedDeliveryPolicy;\n\nconst SETTINGS = ['feedback', 'solutions', 'hints'] as const;\nconst AI_SETTINGS = ['hints', 'explanations'] as const;\n\n/**\n * One setting as a render reads it. Absent or `null` is unset, and unset is\n * `true`: JSON has no `undefined`, so a setting nobody chose arrives from a\n * database as `null`.\n *\n * Anything else that is not exactly `true` reads as `false`. Every setting is a\n * restriction, so a value that cannot be read is read as the restriction — a\n * `\"false\"` string from a form, say, whose author meant exactly that. Reading\n * it as `true` would put hints on a paper whose school had switched them off.\n */\nfunction allows(value: unknown): boolean {\n return value === undefined || value === null || value === true;\n}\n\n/**\n * A policy with every setting spelled out — see {@link DeliveryPolicy}.\n *\n * Tolerant, because it runs where a learner is waiting: nothing here throws.\n * A value it cannot read restricts rather than allows (see above), and a\n * policy that is not an object at all restricts everything. Check a policy\n * before you store it with {@link validateDeliveryPolicy}; `planAttempt` does.\n */\nexport function resolveDeliveryPolicy(policy: unknown): ResolvedDeliveryPolicy {\n if (policy === undefined || policy === null) {\n return OPEN_DELIVERY_POLICY;\n }\n if (typeof policy !== 'object' || Array.isArray(policy)) {\n return {\n feedback: false,\n solutions: false,\n hints: false,\n ai: { hints: false, explanations: false },\n };\n }\n const given = policy as Record<string, unknown>;\n const ai = given.ai;\n // `true` and `false` are shorthand for both AI settings at once. `true`\n // restricts nothing, which is also what leaving `ai` out does — so reading it\n // generously cannot switch on anything a school did not already allow.\n const aiGiven =\n ai === undefined || ai === null || ai === true\n ? {}\n : typeof ai === 'object' && !Array.isArray(ai)\n ? (ai as Record<string, unknown>)\n : { hints: false, explanations: false };\n return {\n feedback: allows(given.feedback),\n solutions: allows(given.solutions),\n hints: allows(given.hints),\n ai: { hints: allows(aiGiven.hints), explanations: allows(aiGiven.explanations) },\n };\n}\n\n/**\n * The strictest of several policies: a setting is on only where every one of\n * them leaves it on. How a course's policy and an assessment's combine, and\n * how a pager's policy wins over whatever a question it draws was handed.\n */\nexport function combineDeliveryPolicies(...policies: unknown[]): ResolvedDeliveryPolicy {\n const resolved = policies.map(resolveDeliveryPolicy);\n const every = (read: (one: ResolvedDeliveryPolicy) => boolean): boolean => resolved.every(read);\n return {\n feedback: every((one) => one.feedback),\n solutions: every((one) => one.solutions),\n hints: every((one) => one.hints),\n ai: {\n hints: every((one) => one.ai.hints),\n explanations: every((one) => one.ai.explanations),\n },\n };\n}\n\n/**\n * Checks a policy before it is stored: each setting `true`, `false`, `null`\n * or absent, and nothing else — no misspelled setting, no string standing in\n * for a boolean.\n *\n * Stricter than {@link resolveDeliveryPolicy} on purpose. A misspelled\n * restriction (`hint: false`) is silently no restriction at all, and a\n * policy is a record of the conditions a paper was sat under: it is worth\n * refusing before it is written down, rather than read charitably after.\n */\nexport function validateDeliveryPolicy(\n policy: unknown,\n): { success: true; data: DeliveryPolicy } | { success: false; issues: DeliveryPolicyIssue[] } {\n const issues: DeliveryPolicyIssue[] = [];\n if (policy === undefined || policy === null) {\n return { success: true, data: {} };\n }\n if (typeof policy !== 'object' || Array.isArray(policy)) {\n return {\n success: false,\n issues: [{ path: '', message: 'A delivery policy is an object of settings.' }],\n };\n }\n const given = policy as Record<string, unknown>;\n for (const key of Object.keys(given)) {\n if (key === 'ai') {\n continue;\n }\n if (!(SETTINGS as readonly string[]).includes(key)) {\n issues.push({\n path: key,\n message: `\"${key}\" is not a delivery setting. The settings are feedback, solutions, hints and ai.`,\n });\n continue;\n }\n const value = given[key];\n if (value !== true && value !== false && value !== null && value !== undefined) {\n issues.push({ path: key, message: `\"${key}\" must be true, false or null.` });\n }\n }\n const ai = given.ai;\n if (ai !== undefined && ai !== null && ai !== true && ai !== false) {\n if (typeof ai !== 'object' || Array.isArray(ai)) {\n issues.push({\n path: 'ai',\n message: '\"ai\" must be true, false, or an object of hints and explanations.',\n });\n } else {\n for (const [key, value] of Object.entries(ai as Record<string, unknown>)) {\n if (!(AI_SETTINGS as readonly string[]).includes(key)) {\n issues.push({\n path: `ai.${key}`,\n message: `\"ai.${key}\" is not an AI setting. The settings are hints and explanations.`,\n });\n continue;\n }\n if (value !== true && value !== false && value !== null && value !== undefined) {\n issues.push({ path: `ai.${key}`, message: `\"ai.${key}\" must be true, false or null.` });\n }\n }\n }\n }\n return issues.length > 0\n ? { success: false, issues }\n : { success: true, data: given as DeliveryPolicy };\n}\n","import { seededShuffle } from './shuffle.js';\nimport type { ItemGroup, MediaTimeline, SequenceEntry, SequenceSlot } from './types/item-group.js';\n\n/** Narrows a sequence entry to an item group. */\nexport function isItemGroup<TItem extends { type: string }>(\n entry: SequenceEntry<TItem>,\n): entry is ItemGroup<TItem> {\n return entry.type === 'item-group';\n}\n\n/**\n * An entry's declared `slotKey`, if it has one. Read structurally rather than\n * from the type, because a plain activity may carry one too — `slotKey` is a\n * property of an item's PLACE in a paper, so any entry can declare it without\n * every activity schema having to know about assessment assembly.\n *\n * A key containing `.` is rejected: `.` separates a group from its item in a\n * slot id, so `\"a.b\"` as a group key would be indistinguishable from item `b`\n * of group `a`.\n */\nfunction keyOf(entry: unknown): string | undefined {\n const key = (entry as { slotKey?: unknown }).slotKey;\n if (key === undefined) {\n return undefined;\n }\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `flattenSequence: slotKey must be a non-empty string, received ${String(key)}.`,\n );\n }\n if (key.includes('.')) {\n throw new Error(\n `flattenSequence: slotKey \"${key}\" contains a \".\" , which separates a group from its item ` +\n 'in a slot id. Choose a key without it, or the two become indistinguishable.',\n );\n }\n return key;\n}\n\n/**\n * An interactive video's items in the order its quizzes open: by the quiz's\n * time — stably, so quizzes at one moment keep their authored order — then by\n * position within the quiz. An item no quiz places (content the schema would\n * refuse) keeps its authored place after every placed one, rather than\n * vanishing from the paper.\n */\nfunction inQuizOrder<TItem extends { id: string }>(\n items: readonly { item: TItem; itemIndex: number }[],\n timeline: MediaTimeline,\n): { item: TItem; itemIndex: number }[] {\n const placement = new Map<string, { at: number; cue: number; position: number }>();\n timeline.cues.forEach((cue, cueIndex) => {\n cue.itemIds.forEach((itemId, position) => {\n if (!placement.has(itemId)) {\n placement.set(itemId, { at: cue.at, cue: cueIndex, position });\n }\n });\n });\n const unplaced = { at: Number.POSITIVE_INFINITY, cue: Number.POSITIVE_INFINITY, position: 0 };\n return items\n .map((entry) => ({ entry, place: placement.get(entry.item.id) ?? unplaced }))\n .sort(\n (a, b) =>\n a.place.at - b.place.at ||\n a.place.cue - b.place.cue ||\n a.place.position - b.place.position ||\n a.entry.itemIndex - b.entry.itemIndex,\n )\n .map(({ entry }) => entry);\n}\n\n/** Options for {@link flattenSequence}. */\nexport interface FlattenSequenceOptions {\n /**\n * Shuffle the top-level entries. A group moves as ONE block — its items are\n * never interleaved with other entries — which is the reason a group exists\n * as a container rather than as a flag on each item.\n */\n shuffleEntries?: boolean;\n /**\n * Seed for every shuffle in this call: the entries, and each group whose\n * `shuffle` is `within-group`. REQUIRED whenever anything shuffles. The SDK\n * never invents one, because the server that stores an attempt and the\n * client that renders it must derive the SAME order, and only a shared seed\n * makes that true. The attempt id is the natural choice.\n */\n seed?: string;\n}\n\n/**\n * Turns a sequence definition — loose activities and item groups, in authored\n * order — into the ordered list of slots to present. Pure and deterministic:\n * call it on the server to record an attempt's order, and on the client to\n * render it, and the two agree.\n *\n * Shuffling is opt-in and seeded. Groups are shuffle-atomic: with\n * `shuffleEntries` a group changes position but stays one contiguous block,\n * and only a group that declares `shuffle: 'within-group'` has its items\n * reordered. Every slot carries a `slotId` derived from the authored\n * position, so identities survive shuffling.\n *\n * @throws Error when a shuffle is requested without a `seed`.\n */\nexport function flattenSequence<TItem extends { id: string; type: string }>(\n entries: readonly SequenceEntry<TItem>[],\n options: FlattenSequenceOptions = {},\n): SequenceSlot<TItem>[] {\n const shuffleEntries = options.shuffleEntries === true;\n const needsSeed =\n shuffleEntries ||\n entries.some((entry) => isItemGroup(entry) && entry.shuffle === 'within-group');\n if (needsSeed && options.seed === undefined) {\n throw new Error(\n 'flattenSequence: a seed is required when shuffling (shuffleEntries, or a group with ' +\n 'shuffle: \"within-group\"). Pass the attempt id so the server and the client derive the same order.',\n );\n }\n const seed = options.seed ?? '';\n\n // Carry the AUTHORED index through the shuffle: slot identity comes from\n // where an entry was written, never from where it happens to be shown.\n const authored = entries.map((entry, entryIndex) => ({ entry, entryIndex }));\n const ordered = shuffleEntries\n ? seededShuffle(authored, `${seed}:entries`, { version: 1 })\n : authored;\n\n const slots: SequenceSlot<TItem>[] = [];\n for (const { entry, entryIndex } of ordered) {\n // An authored `slotKey` wins over the positional path. A positional id is\n // only valid against ONE version of the entries array; a declared key\n // survives insertion, deletion and re-ordering, which is what makes a\n // stored slot id safe to re-grade against later.\n const entryKey = keyOf(entry) ?? String(entryIndex);\n if (!isItemGroup(entry)) {\n slots.push({ slotId: entryKey, index: slots.length, activity: entry });\n continue;\n }\n // An empty group contributes no slots, so it would DISAPPEAR — stimulus,\n // questions and all — from a sequence that still looks well-formed. A\n // listening section filtered to nothing upstream would leave an 18-slot\n // paper presented as 12 slots, and `composeAssessmentScore` would then\n // report a `final` grade over the survivors with nothing pending. The\n // type already declares `items` non-empty; this makes that enforceable at\n // the point where the omission would otherwise become invisible.\n if (entry.items.length === 0) {\n throw new Error(\n `flattenSequence: item group \"${entry.id}\" has no items. An empty group would silently ` +\n 'remove its stimulus and its questions from the presented sequence.',\n );\n }\n const timeline = entry.timeline;\n // A defence behind the schema's refusal: the two orders cannot both hold,\n // and a seeded shuffle of a video's questions would detach each one from\n // the moment it asks about.\n if (timeline !== undefined && entry.shuffle === 'within-group') {\n throw new Error(\n `flattenSequence: item group \"${entry.id}\" has a timeline and shuffle: \"within-group\". ` +\n 'An interactive video presents its questions in the order its quizzes open.',\n );\n }\n const items = entry.items.map((item, itemIndex) => ({ item, itemIndex }));\n const presented =\n timeline !== undefined\n ? inQuizOrder(items, timeline)\n : entry.shuffle === 'within-group'\n ? seededShuffle(items, `${seed}:group:${entry.id}`, { version: 1 })\n : items;\n const size = presented.length;\n presented.forEach(({ item, itemIndex }, position) => {\n const cue = timeline?.cues.find((candidate) => candidate.itemIds.includes(item.id));\n slots.push({\n // Slot ids stay AUTHORED — the item's index or its key — whatever the\n // presented order: moving a quiz must never re-map a stored answer.\n slotId: `${entryKey}.${keyOf(item) ?? String(itemIndex)}`,\n index: slots.length,\n activity: item,\n group: {\n id: entry.id,\n ...(entry.title !== undefined ? { title: entry.title } : {}),\n stimulus: entry.stimulus,\n position,\n size,\n ...(timeline !== undefined ? { timeline } : {}),\n ...(cue !== undefined ? { cue } : {}),\n },\n });\n });\n }\n\n // Positional ids are unique by construction; authored keys are not. Two\n // entries sharing a key would collapse into one identity, so every response\n // stored against it would overwrite the other's — and `composeAssessmentScore`\n // would score one question twice and the other never.\n const seenSlotIds = new Set<string>();\n // ENTRY keys are checked separately, because a loose entry keyed \"reading\"\n // and a group keyed \"reading\" produce slot ids \"reading\" and \"reading.0\"\n // that never collide — while everything reading the entry prefix (the\n // pager's stimulus grouping, for one) treats them as the same entry, and\n // shows the group's passage above the unrelated loose question. Positional\n // ids could not express this: an index is a loose item or a group, never\n // both.\n const seenEntryKeys = new Set<string>();\n for (const slot of slots) {\n if (seenSlotIds.has(slot.slotId)) {\n throw new Error(\n `flattenSequence: duplicate slot id \"${slot.slotId}\". Two entries declare the same ` +\n 'slotKey, so responses stored against them could not be told apart.',\n );\n }\n seenSlotIds.add(slot.slotId);\n }\n for (const { entry, entryIndex } of ordered) {\n const entryKey = keyOf(entry) ?? String(entryIndex);\n if (seenEntryKeys.has(entryKey)) {\n throw new Error(\n `flattenSequence: duplicate entry key \"${entryKey}\". A loose activity and an item group ` +\n 'cannot share a slotKey — their slot ids would not collide, but everything that reads ' +\n 'the entry they belong to would treat them as one entry.',\n );\n }\n seenEntryKeys.add(entryKey);\n }\n return slots;\n}\n","import type { ActivityMedia, NativeControlHint } from './types/activity.js';\nimport type { AttemptPlan } from './types/attempt-plan.js';\nimport type { MediaPlayLedger, MediaPlayLedgerEntry } from './types/media-budget.js';\n\nexport type {\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedger,\n MediaPlayLedgerEntry,\n} from './types/media-budget.js';\n\n/**\n * The authored-entry prefix of a slot id: `\"3\"` for both `\"3\"` and `\"3.1\"`.\n *\n * Safe because every slot id in a plan is produced by `flattenSequence`, and\n * `keyOf` rejects a `.` in both an entry key and an item key — so the first\n * `.` is always the group/item separator.\n */\nexport function entryKeyOf(slotId: string): string {\n const dot = slotId.indexOf('.');\n return dot === -1 ? slotId : slotId.slice(0, dot);\n}\n\n/** Budget key for an activity's OWN `data.media`. One budget per slot. */\nexport function slotMediaKey(slotId: string): string {\n return `slot:${slotId}`;\n}\n\n/**\n * Budget key for an item group's `stimulus.media`.\n *\n * Keyed by the ENTRY, not the slot: one recording serves every question in the\n * group, so `\"3.0\"`…`\"3.5\"` share one budget. Keying by `slotId` would hand a\n * six-question `maxPlays: 2` listening group twelve plays; keying by\n * `stimulus.id` would collide across papers, which is the thing slot keys and\n * `planHash` exist to prevent.\n */\nexport function stimulusMediaKey(slotId: string): string {\n return `stimulus:${entryKeyOf(slotId)}`;\n}\n\n/** A playback policy with every default resolved. One derivation, used everywhere. */\nexport interface ResolvedPlaybackPolicy {\n controls: 'native' | 'minimal';\n /** `null` when the recording is unbudgeted. */\n maxPlays: number | null;\n seek: 'allow' | 'none';\n rate: 'allow' | 'fixed';\n nativeControlHints: readonly NativeControlHint[];\n}\n\n/**\n * Resolves the defaults once, so the schema refinements, the plan and the\n * renderer cannot drift apart about what a policy means.\n *\n * Absent `playback` resolves to exactly the SDK's pre-0.8.0 behaviour:\n * `controls: 'native'`, no budget, free seeking, free speed, no hints.\n */\nexport function resolvePlaybackPolicy(media: ActivityMedia): ResolvedPlaybackPolicy {\n const p = media.playback;\n const budgeted = typeof p?.maxPlays === 'number';\n const seek = p?.seek ?? (budgeted ? 'none' : 'allow');\n const rate = p?.rate ?? 'allow';\n const controls =\n p?.controls ?? (budgeted || seek === 'none' || rate === 'fixed' ? 'minimal' : 'native');\n return {\n controls,\n maxPlays: budgeted ? (p?.maxPlays as number) : null,\n seek,\n rate,\n nativeControlHints: p?.nativeControlHints ?? [],\n };\n}\n\n/**\n * Every budgeted recording a plan contains: budget key → the `maxPlays` that\n * was frozen when the attempt was planned.\n *\n * Use it to build the storage rows an attempt needs, and to check a stored\n * ledger against the paper it claims to belong to.\n */\nexport function planMediaBudgets(plan: AttemptPlan): Record<string, number> {\n const out: Record<string, number> = {};\n for (const slot of plan.slots) {\n for (const budget of slot.mediaBudgets ?? []) {\n out[budget.key] = budget.maxPlays;\n }\n }\n return out;\n}\n\nfunction assertEntry(key: string, entry: MediaPlayLedgerEntry): void {\n if (\n entry === null ||\n typeof entry !== 'object' ||\n !Number.isInteger(entry.plays) ||\n entry.plays < 0\n ) {\n throw new Error(\n `serializeMediaPlayLedger: play count for ${JSON.stringify(key)} is ` +\n `${JSON.stringify((entry as MediaPlayLedgerEntry | null)?.plays)}. A count must be a ` +\n 'non-negative integer; Number(a NULL column) is the usual cause.',\n );\n }\n if (entry.at !== undefined && (!Number.isFinite(entry.at) || entry.at < 0)) {\n throw new Error(\n `serializeMediaPlayLedger: position for ${JSON.stringify(key)} is ` +\n `${JSON.stringify(entry.at)}. A position must be a finite number of seconds >= 0.`,\n );\n }\n}\n\n/**\n * Validates a set of spent-play counts against the plan that budgeted them,\n * and stamps the envelope.\n *\n * Validates on the way **in**: a play recorded against a recording the paper\n * does not budget is a bug at the moment it is written, and discovering it\n * when a learner tries to resume is discovering it far too late.\n *\n * @throws Error when a key is not budgeted by the plan, or a count is not a\n * non-negative integer, or a position is not a finite number.\n */\nexport function serializeMediaPlayLedger(\n plan: AttemptPlan,\n entries: Readonly<Record<string, MediaPlayLedgerEntry>>,\n options: { savedAt?: string } = {},\n): MediaPlayLedger {\n if (entries === null || typeof entries !== 'object') {\n throw new Error('serializeMediaPlayLedger: entries must be an object keyed by media key.');\n }\n const budgets = planMediaBudgets(plan);\n const out: Record<string, MediaPlayLedgerEntry> = {};\n\n for (const key of Object.keys(entries)) {\n if (!Object.hasOwn(budgets, key)) {\n throw new Error(\n `serializeMediaPlayLedger: media play recorded against a recording the plan does not ` +\n `budget: ${JSON.stringify(key)}. Keys come from slotMediaKey() / stimulusMediaKey(); a ` +\n 'key that belongs to no budgeted media means the client and the plan disagree about ' +\n 'which paper this is.',\n );\n }\n const entry = entries[key] as MediaPlayLedgerEntry;\n assertEntry(key, entry);\n out[key] = { plays: entry.plays, ...(entry.at !== undefined ? { at: entry.at } : {}) };\n }\n\n return {\n ledgerVersion: '1.0',\n planHash: plan.planHash,\n entries: out,\n ...(options.savedAt !== undefined ? { savedAt: options.savedAt } : {}),\n };\n}\n\n/**\n * Reopens a stored ledger against the paper it was spent on.\n *\n * Refuses a ledger from a different paper for the same reason\n * `restoreAttemptState` does: budget keys are short and repeat across papers\n * (`slot:0`, `stimulus:1`), so a ledger from last term's midterm would hold a\n * learner to a budget from an exam they never sat, and look entirely plausible\n * doing it.\n *\n * @throws Error on an unrecognised `ledgerVersion`, a `planHash` mismatch, or\n * anything `serializeMediaPlayLedger` refuses.\n */\nexport function restoreMediaPlayLedger(\n plan: AttemptPlan,\n stored: MediaPlayLedger,\n): MediaPlayLedger {\n if (stored === null || typeof stored !== 'object') {\n throw new Error('restoreMediaPlayLedger: the stored ledger is not an object.');\n }\n if (stored.ledgerVersion !== '1.0') {\n throw new Error(\n `restoreMediaPlayLedger: unsupported ledgerVersion ${JSON.stringify(stored.ledgerVersion)}. ` +\n 'This build understands \"1.0\"; a newer ledger must be migrated before it is restored.',\n );\n }\n if (stored.entries === null || typeof stored.entries !== 'object') {\n throw new Error(\n 'restoreMediaPlayLedger: the ledger carries no entries object (a NULL column, or a ' +\n 'partially written row).',\n );\n }\n if (stored.planHash !== plan.planHash) {\n throw new Error(\n 'restoreMediaPlayLedger: this ledger belongs to a different paper ' +\n `(ledger ${stored.planHash}, plan ${plan.planHash}). Restoring it would hold the learner ` +\n 'to a play budget from an exam they never sat.',\n );\n }\n return serializeMediaPlayLedger(plan, stored.entries, {\n ...(stored.savedAt !== undefined ? { savedAt: stored.savedAt } : {}),\n });\n}\n","import { contentHash } from './content-hash.js';\nimport { resolveDeliveryPolicy, validateDeliveryPolicy } from './delivery.js';\nimport { flattenSequence } from './item-group.js';\nimport { resolvePlaybackPolicy, slotMediaKey, stimulusMediaKey } from './media-budget.js';\nimport type { ScoredItem } from './scoring/compose.js';\nimport { resolveItemScoringPolicy, validateItemScoringPolicy } from './scoring/item-scoring.js';\nimport type { ActivityData, ActivityMedia, ItemOutcome } from './types/activity.js';\nimport type {\n AttemptPlan,\n AttemptPlanDrift,\n AttemptPlanSlot,\n MediaBudgetRef,\n} from './types/attempt-plan.js';\nimport type { DeliveryPolicy, ResolvedDeliveryPolicy } from './types/delivery.js';\nimport type { SequenceEntry, SequenceSlot } from './types/item-group.js';\nimport type { ItemScoringPolicy, ResolvedItemScoringPolicy } from './types/item-scoring.js';\n\nexport type {\n AttemptPlan,\n AttemptPlanDrift,\n AttemptPlanSlot,\n MediaBudgetRef,\n} from './types/attempt-plan.js';\n\n/** Options for {@link planAttempt}. */\nexport interface PlanAttemptOptions<TItem> {\n /** Shuffle the top-level entries. A group moves as one block. */\n shuffleEntries?: boolean;\n /**\n * Seed for every shuffle in this attempt. Required whenever anything\n * shuffles — the attempt has to be reproducible, which is the entire point\n * of writing a plan down. Use the attempt id.\n */\n seed?: string;\n /**\n * What each slot is worth. Defaults to 1 for every slot.\n *\n * Points are resolved HERE, not read from content, because they belong to\n * the paper rather than the item: the same question is worth 1 in a practice\n * quiz and 3 in a final. Whatever this returns is frozen into the plan and\n * is what `composeAssessmentScore` will weight by.\n *\n * @throws Error when it returns a value that is not a finite, non-negative\n * number — a slot worth `NaN` points would poison the whole total.\n */\n points?: (slot: SequenceSlot<TItem>) => number;\n /**\n * The delivery policy this attempt is sat under — whether hints were\n * offered, whether AI help was, whether feedback and solutions showed.\n * Frozen into the plan with every setting spelled out, so the record of the\n * conditions a grade was earned under does not depend on today's defaults.\n *\n * Recorded only when given: a plan made without one is byte-identical to a\n * plan made before policies existed, `planHash` included.\n *\n * @throws Error when the policy does not pass `validateDeliveryPolicy` —\n * a misspelled restriction is no restriction, and a plan is the\n * record an appeal reads.\n */\n delivery?: DeliveryPolicy | null;\n /**\n * The scoring policy this attempt is sat under — how many tries each\n * question gives, which one counts, and what tries and hints cost. Frozen\n * into the plan with every setting spelled out, like `delivery`, and for a\n * stronger reason: every setting in it moves a grade, so a re-grade must\n * apply the rules the learner sat under, not today's.\n *\n * Recorded only when given: a plan made without one is byte-identical to a\n * plan made before scoring policies existed, `planHash` included.\n *\n * @throws Error when the policy does not pass `validateItemScoringPolicy`.\n */\n scoring?: ItemScoringPolicy | null;\n}\n\n/**\n * The activity as CONTENT, with its slot key removed.\n *\n * `slotKey` rides on the same object but is identity, not content — the whole\n * point of the design. Including it in the fingerprint meant that annotating\n * an existing entry with the key that pins its identity reported as\n * \"this question was edited\", which is precisely backwards.\n */\nfunction contentOf(activity: object): object {\n if (!Object.hasOwn(activity, 'slotKey')) {\n return activity;\n }\n const { slotKey: _slotKey, ...content } = activity as { slotKey?: unknown };\n return content;\n}\n\n/**\n * The budgeted recordings a slot presents: its own media, and its group's\n * stimulus. Returns `undefined` — not `[]` — when nothing is budgeted, so the\n * conditional spread below leaves the serialized slot byte-identical to what\n * every pre-0.8.0 paper produced.\n */\nfunction mediaBudgetsOf<TItem extends { id: string; type: string }>(\n slot: SequenceSlot<TItem>,\n): MediaBudgetRef[] | undefined {\n const budgets: MediaBudgetRef[] = [];\n // Read structurally: `media` is a shared optional field, not part of the\n // `{ id, type }` bound this function is generic over.\n const own = (slot.activity as { media?: ActivityMedia }).media;\n if (own !== undefined) {\n const maxPlays = resolvePlaybackPolicy(own).maxPlays;\n if (maxPlays !== null) {\n budgets.push({ key: slotMediaKey(slot.slotId), maxPlays });\n }\n }\n const stimulusMedia = slot.group?.stimulus.media;\n if (stimulusMedia !== undefined) {\n const maxPlays = resolvePlaybackPolicy(stimulusMedia).maxPlays;\n if (maxPlays !== null) {\n budgets.push({ key: stimulusMediaKey(slot.slotId), maxPlays });\n }\n }\n return budgets.length > 0 ? budgets : undefined;\n}\n\n/**\n * Refuses a paper in which one recording is budgeted under several keys.\n *\n * Six questions that each carry the same `/audio/part2.mp3` with `maxPlays: 2`\n * are six budgets, so the learner gets twelve plays of one recording while the\n * paper says two. The fix is authoring, not arithmetic: questions that share a\n * recording belong in an item group, whose stimulus is one recording with one\n * budget.\n */\nfunction assertNoSplitBudgets<TItem extends { id: string; type: string }>(\n slots: readonly SequenceSlot<TItem>[],\n): void {\n const keysByUrl = new Map<string, string[]>();\n for (const slot of slots) {\n const own = (slot.activity as { media?: ActivityMedia }).media;\n if (own === undefined || resolvePlaybackPolicy(own).maxPlays === null) {\n continue;\n }\n const keys = keysByUrl.get(own.url) ?? [];\n keys.push(slotMediaKey(slot.slotId));\n keysByUrl.set(own.url, keys);\n }\n for (const [url, keys] of keysByUrl) {\n if (keys.length > 1) {\n throw new Error(\n `planAttempt: media ${JSON.stringify(url)} is budgeted under ${keys.length} separate ` +\n `keys (${keys.join(', ')}), so one recording grants ${keys.length} × maxPlays. Put the ` +\n \"questions that share a recording in an item group — a group's stimulus is one \" +\n 'recording with one budget.',\n );\n }\n }\n}\n\nfunction planSlot<TItem extends { id: string; type: string }>(\n slot: SequenceSlot<TItem>,\n points: number,\n): AttemptPlanSlot {\n if (!Number.isFinite(points) || points < 0) {\n throw new Error(\n `planAttempt: slot \"${slot.slotId}\" resolved to ${String(points)} points. Points must be a ` +\n 'finite, non-negative number, or the paper has no defensible total.',\n );\n }\n const mediaBudgets = mediaBudgetsOf(slot);\n return {\n slotId: slot.slotId,\n index: slot.index,\n activityId: slot.activity.id,\n activityType: slot.activity.type,\n points,\n contentHash: contentHash(contentOf(slot.activity)),\n ...(slot.group !== undefined\n ? {\n group: {\n id: slot.group.id,\n ...(slot.group.title !== undefined ? { title: slot.group.title } : {}),\n stimulusHash: contentHash(slot.group.stimulus),\n ...(slot.group.cue !== undefined\n ? {\n cue: {\n id: slot.group.cue.id,\n at: slot.group.cue.at,\n ...(slot.group.cue.required !== undefined\n ? { required: slot.group.cue.required }\n : {}),\n },\n }\n : {}),\n },\n }\n : {}),\n ...(mediaBudgets !== undefined ? { mediaBudgets } : {}),\n };\n}\n\n/**\n * Freezes what an attempt is being served: the presented order, each slot's\n * identity and worth, and a fingerprint of the content behind it.\n *\n * Call this once, when the attempt starts, and store the result beside the\n * responses. Everything that decides the grade is then a historical fact\n * rather than a re-read of content that may since have changed — which is what\n * makes a re-grade, a remark or an appeal answerable.\n *\n * Ordering comes from `flattenSequence`, so a plan and a live render of the\n * same entries with the same seed agree slot for slot.\n *\n * @throws Error when a shuffle is requested without a seed, when a group is\n * empty, when two entries collide on a `slotKey`, or when `points`\n * returns a value that is not finite and non-negative.\n */\nexport function planAttempt<TItem extends { id: string; type: string }>(\n entries: readonly SequenceEntry<TItem>[],\n options: PlanAttemptOptions<TItem> = {},\n): AttemptPlan {\n const { seed, shuffleEntries, points } = options;\n const delivery = frozenDelivery(options.delivery);\n const scoring = frozenScoring(options.scoring);\n const slots = flattenSequence(entries, {\n ...(shuffleEntries !== undefined ? { shuffleEntries } : {}),\n ...(seed !== undefined ? { seed } : {}),\n });\n\n assertNoSplitBudgets(slots);\n\n const planned = slots.map((slot) => planSlot(slot, points?.(slot) ?? 1));\n const totalPoints = planned.reduce((sum, slot) => sum + slot.points, 0);\n\n return {\n planVersion: '1.0',\n ...(seed !== undefined ? { seed } : {}),\n ...(shuffleEntries !== undefined ? { shuffleEntries } : {}),\n // The plan's own fingerprint covers the slots verbatim — identity, order,\n // points and content hashes — so one stored value answers \"is this still\n // the paper that was sat?\" and the per-slot hashes then say what moved.\n // A policy joins it when there is one: the same paper sat with hints and\n // without is two different attempts. Without one, the fingerprint is the\n // one every plan before policies had.\n // So does a scoring policy: the same answers are worth something else when\n // a hint costs a tenth of the marks. Each joins only when it is there, so\n // a plan with neither — or with a delivery policy alone — keeps the\n // fingerprint it always had.\n planHash: contentHash(\n delivery === undefined && scoring === undefined\n ? planned\n : {\n slots: planned,\n ...(delivery !== undefined ? { delivery } : {}),\n ...(scoring !== undefined ? { scoring } : {}),\n },\n ),\n ...(delivery !== undefined ? { delivery } : {}),\n ...(scoring !== undefined ? { scoring } : {}),\n slots: planned,\n totalPoints,\n };\n}\n\n/**\n * Compares a stored plan with the content as it stands now, and reports every\n * way they have drifted apart.\n *\n * This is the question a remark or an appeal actually asks: *is the paper I am\n * looking at the paper this learner sat?* Ids alone cannot answer it, because\n * they survive an edit unchanged.\n *\n * Rebuild the comparison plan with the SAME options the stored one recorded —\n * its `seed`, its `shuffleEntries`, and the same `points` function — or the\n * differences you see will be your own:\n *\n * ```ts\n * const now = planAttempt(currentEntries, {\n * seed: stored.seed,\n * shuffleEntries: stored.shuffleEntries,\n * points: pointsFor, // omit it and every slot reweights to 1\n * });\n * const drift = verifyAttemptPlan(stored, now);\n * ```\n *\n * A drift is not automatically a problem — a fixed typo changes a hash without\n * changing what was asked. It is a fact somebody has to be able to see.\n */\nexport function verifyAttemptPlan(plan: AttemptPlan, current: AttemptPlan): AttemptPlanDrift {\n const before = new Map(plan.slots.map((slot) => [slot.slotId, slot]));\n const after = new Map(current.slots.map((slot) => [slot.slotId, slot]));\n\n const missingSlotIds = plan.slots.filter((s) => !after.has(s.slotId)).map((s) => s.slotId);\n const addedSlotIds = current.slots.filter((s) => !before.has(s.slotId)).map((s) => s.slotId);\n\n const changedSlotIds: string[] = [];\n const changedStimulusSlotIds: string[] = [];\n const changedCueSlotIds: string[] = [];\n const changedPointsSlotIds: string[] = [];\n const reorderedSlotIds: string[] = [];\n // Not a slot's: the conditions of the whole attempt. Compared as spelled out,\n // so a policy recorded before and one recorded now differ only where a\n // setting does.\n const deliveryChanged =\n contentHash(plan.delivery ?? null) !== contentHash(current.delivery ?? null);\n const scoringChanged = contentHash(plan.scoring ?? null) !== contentHash(current.scoring ?? null);\n for (const slot of plan.slots) {\n const now = after.get(slot.slotId);\n if (now === undefined) {\n continue;\n }\n // A swapped activity changes the content behind the slot, so it lands in\n // `changedSlotIds` through the fingerprint rather than needing its own arm.\n if (now.contentHash !== slot.contentHash) {\n changedSlotIds.push(slot.slotId);\n }\n if (now.group?.stimulusHash !== slot.group?.stimulusHash) {\n changedStimulusSlotIds.push(slot.slotId);\n }\n if (contentHash(now.group?.cue ?? null) !== contentHash(slot.group?.cue ?? null)) {\n changedCueSlotIds.push(slot.slotId);\n }\n // A reweight moves the grade without touching a single question. Left out,\n // `matches` said the paper was unchanged while its `planHash` disagreed.\n if (now.points !== slot.points) {\n changedPointsSlotIds.push(slot.slotId);\n }\n if (now.index !== slot.index) {\n reorderedSlotIds.push(slot.slotId);\n }\n }\n\n return {\n matches:\n missingSlotIds.length === 0 &&\n addedSlotIds.length === 0 &&\n changedSlotIds.length === 0 &&\n changedStimulusSlotIds.length === 0 &&\n changedCueSlotIds.length === 0 &&\n changedPointsSlotIds.length === 0 &&\n reorderedSlotIds.length === 0 &&\n !deliveryChanged &&\n !scoringChanged,\n missingSlotIds,\n addedSlotIds,\n changedSlotIds,\n changedStimulusSlotIds,\n changedCueSlotIds,\n changedPointsSlotIds,\n reorderedSlotIds,\n // Present only when it is true, as `rejectedSlotIds` is on a composed\n // score: a drift report between two plans without a policy — every one\n // written before policies existed — keeps exactly the shape it had.\n ...(deliveryChanged ? { deliveryChanged: true as const } : {}),\n ...(scoringChanged ? { scoringChanged: true as const } : {}),\n };\n}\n\n/**\n * The policy a plan records: checked, then spelled out. `undefined` when there\n * is none, so the plan carries no field at all rather than an empty one.\n */\nfunction frozenDelivery(policy: unknown): ResolvedDeliveryPolicy | undefined {\n if (policy === undefined || policy === null) {\n return undefined;\n }\n const checked = validateDeliveryPolicy(policy);\n if (!checked.success) {\n const first = checked.issues[0];\n throw new Error(\n `planAttempt: the delivery policy is not one to record — ${first?.message ?? 'invalid'}` +\n (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ''),\n );\n }\n return resolveDeliveryPolicy(checked.data);\n}\n\n/** The scoring policy a plan records, as {@link frozenDelivery} records a delivery policy. */\nfunction frozenScoring(policy: unknown): ResolvedItemScoringPolicy | undefined {\n if (policy === undefined || policy === null) {\n return undefined;\n }\n const checked = validateItemScoringPolicy(policy);\n if (!checked.success) {\n const first = checked.issues[0];\n throw new Error(\n `planAttempt: the scoring policy is not one to record — ${first?.message ?? 'invalid'}` +\n (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ''),\n );\n }\n return resolveItemScoringPolicy(checked.data);\n}\n\n/** How {@link scoredItemsFromPlan} fills a slot that has no recorded outcome. */\nexport type MissingOutcomePolicy =\n /**\n * Default. `{ status: 'deferred', reason: 'no_response_recorded' }` — a\n * non-terminal state, so the composed result stays `provisional` and cannot\n * be recorded as a final pass or fail.\n */\n | 'deferred'\n /**\n * `{ status: 'scored', score: 0 }` — the learner left it blank on a paper\n * that IS complete. Choose this only when you know the attempt was\n * submitted; it is a real zero and makes the result final.\n */\n | 'zero'\n /** Build the outcome yourself, per slot. */\n | ((slot: AttemptPlanSlot) => ItemOutcome);\n\nfunction missingOutcome(slot: AttemptPlanSlot, policy: MissingOutcomePolicy): ItemOutcome {\n if (typeof policy === 'function') {\n return policy(slot);\n }\n if (policy === 'zero') {\n return { status: 'scored', score: 0, maxScore: 1, passed: false, feedback: null, details: [] };\n }\n return { status: 'deferred', reason: 'no_response_recorded', maxScore: 1 };\n}\n\n/**\n * Turns a plan plus whatever outcomes exist into the `ScoredItem[]`\n * `composeAssessmentScore` consumes.\n *\n * The plan is the source of the denominator, not the outcomes. A slot the\n * learner never reached still has to appear — otherwise it silently leaves the\n * denominator and the remaining questions quietly become worth more than the\n * paper says.\n *\n * **A missing outcome defaults to `deferred`, never `unscorable`.** The\n * distinction decides a grade: `unscorable` means \"a grade is never coming\",\n * so `composeAssessmentScore` drops the slot from the denominator AND lets the\n * result go `final` — which turned a three-question paper with one answer into\n * a final, passing 100%. `deferred` means \"not yet\", which holds the result\n * `provisional` so nothing can be recorded. Pass `'zero'` once you know the\n * attempt was submitted and the blanks are genuinely blanks.\n *\n * Pass outcomes keyed by `slotId`. Keys the plan does not know are ignored:\n * a plan is the authority on what the attempt contained.\n */\nexport function scoredItemsFromPlan(\n plan: AttemptPlan,\n outcomes: Readonly<Record<string, ItemOutcome>>,\n options: { missing?: MissingOutcomePolicy } = {},\n): ScoredItem[] {\n const policy = options.missing ?? 'deferred';\n return plan.slots.map((slot) => ({\n slotId: slot.slotId,\n activityId: slot.activityId,\n points: slot.points,\n // `Object.hasOwn`, not a bare lookup: slot ids come from authored\n // `slotKey`s, and a key of `constructor` or `toString` would otherwise\n // resolve through the prototype chain and hand a FUNCTION to the scorer\n // in place of an outcome.\n outcome: Object.hasOwn(outcomes, slot.slotId)\n ? (outcomes[slot.slotId] as ItemOutcome)\n : missingOutcome(slot, policy),\n }));\n}\n\n/** Convenience alias: the entry type a plan is built from. */\nexport type PlannableEntry = SequenceEntry<ActivityData>;\n","import { canonicalJson } from './content-hash.js';\nimport type { LearnerResponse } from './types/activity.js';\nimport type { AttemptPlan } from './types/attempt-plan.js';\nimport type { AttemptState, ResponseDiffEntry } from './types/attempt-state.js';\n\nexport type { AttemptState, ResponseDiffEntry } from './types/attempt-state.js';\n\n/** What {@link serializeAttemptState} is given about an attempt in progress. */\nexport interface AttemptProgress {\n /** Answers so far, keyed by `slotId`. */\n responses: Readonly<Record<string, LearnerResponse>>;\n /** Slots already submitted. Defaults to none. */\n submittedSlotIds?: readonly string[];\n /** Presented position the learner is on. Defaults to 0. */\n index?: number;\n /** ISO 8601 timestamp to stamp the snapshot with. The SDK does not read a clock. */\n savedAt?: string;\n}\n\n/**\n * A DEEP copy of the responses.\n *\n * A one-level spread is not enough: every `LearnerResponse` shape is itself an\n * object (`selectedOptionIds`, `answers`, `text`), so a spread hands back a new\n * map of the caller's SAME response objects. A consumer whose reducer updates\n * an answer in place — an Immer draft, a push onto a multi-select, or\n * `answers[blankId] = text`, which is the natural update for that shape — then\n * mutates every snapshot ever taken. The stored snapshot retroactively becomes\n * the new answer, `diffResponses` sees no change, and a delta autosave writes\n * nothing for an edit the learner really made.\n */\nfunction cloneResponses(\n responses: Readonly<Record<string, LearnerResponse>>,\n): Record<string, LearnerResponse> {\n // Responses are JSON by contract, so the fallback is lossless for them.\n return typeof structuredClone === 'function'\n ? structuredClone(responses as Record<string, LearnerResponse>)\n : (JSON.parse(JSON.stringify(responses)) as Record<string, LearnerResponse>);\n}\n\n/** Slot ids present in the object but unknown to the plan. */\nfunction unknownKeys(plan: AttemptPlan, keys: readonly string[]): string[] {\n const known = new Set(plan.slots.map((slot) => slot.slotId));\n return keys.filter((key) => !known.has(key));\n}\n\n/**\n * Captures an in-progress attempt as a storable snapshot, bound to its plan.\n *\n * Validated against the plan on the way in, not on the way out: a response\n * stored under a slot the paper does not contain is a bug at the moment it is\n * written, and finding it months later — when a learner tries to resume — is\n * finding it far too late.\n *\n * The SDK reads no clock: pass `savedAt` if you want the snapshot stamped, so\n * the function stays pure and its output stays reproducible in a test.\n *\n * @throws Error when a response or submitted slot is not in the plan, or when\n * `index` is not a position the plan actually has.\n */\nexport function serializeAttemptState(plan: AttemptPlan, progress: AttemptProgress): AttemptState {\n const responseKeys = Object.keys(progress.responses);\n const submittedSlotIds = [...(progress.submittedSlotIds ?? [])];\n\n const strayResponses = unknownKeys(plan, responseKeys);\n if (strayResponses.length > 0) {\n throw new Error(\n `serializeAttemptState: response recorded for slot(s) the plan does not contain: ${strayResponses.join(', ')}. ` +\n 'A response that belongs to no question cannot be scored and would be lost silently.',\n );\n }\n const straySubmitted = unknownKeys(plan, submittedSlotIds);\n if (straySubmitted.length > 0) {\n throw new Error(\n `serializeAttemptState: submitted slot(s) the plan does not contain: ${straySubmitted.join(', ')}.`,\n );\n }\n\n const index = progress.index ?? 0;\n // An out-of-range position would reopen the attempt on a question that is\n // not there — which the pager renders as an empty shell with no way forward.\n // An empty plan admits only 0: guarding with `slots.length > 0` skipped the\n // range check altogether there, so a zero-slot paper accepted any index and\n // the error message contradicted itself.\n const lastIndex = Math.max(0, plan.slots.length - 1);\n if (!Number.isInteger(index) || index < 0 || index > lastIndex) {\n throw new Error(\n `serializeAttemptState: index ${String(index)} is not a position in a ${plan.slots.length}-slot plan.`,\n );\n }\n\n return {\n stateVersion: '1.0',\n planHash: plan.planHash,\n // Copied DEEPLY, not aliased: a snapshot that keeps mutating with the\n // live attempt is not a snapshot. See cloneResponses.\n responses: cloneResponses(progress.responses),\n submittedSlotIds,\n index,\n ...(progress.savedAt !== undefined ? { savedAt: progress.savedAt } : {}),\n };\n}\n\n/**\n * Reopens a stored snapshot against a plan, refusing anything that does not\n * belong to it.\n *\n * The `planHash` check is the point. Slot ids are short and stable by design,\n * so a snapshot from a DIFFERENT paper — last term's midterm, a sibling\n * version, a copy-pasted attempt row — will happily line its answers up\n * against the wrong questions and look entirely plausible doing it. Comparing\n * the paper's fingerprint is what makes that impossible rather than unlikely.\n *\n * @throws Error when the snapshot belongs to a different plan, or references\n * slots the plan does not contain.\n */\nexport function restoreAttemptState(plan: AttemptPlan, state: AttemptState): AttemptState {\n if (state === null || typeof state !== 'object') {\n throw new Error('restoreAttemptState: the snapshot is not an object.');\n }\n // The one field added so a stored snapshot survives an envelope change was\n // the one field never read: an unrecognised version was accepted,\n // reinterpreted under this version's rules, and re-stamped '1.0' — which\n // also destroyed the evidence that it had ever been anything else.\n if (state.stateVersion !== '1.0') {\n throw new Error(\n `restoreAttemptState: unsupported stateVersion ${JSON.stringify(state.stateVersion)}. ` +\n 'This build understands \"1.0\"; a newer snapshot must be migrated before it is restored.',\n );\n }\n if (state.responses === null || typeof state.responses !== 'object') {\n throw new Error(\n 'restoreAttemptState: the snapshot carries no responses object (a NULL column, or a ' +\n 'partially written row).',\n );\n }\n if (state.planHash !== plan.planHash) {\n throw new Error(\n 'restoreAttemptState: this snapshot belongs to a different paper ' +\n `(snapshot ${state.planHash}, plan ${plan.planHash}). Restoring it would attach the ` +\n \"learner's answers to questions they never saw.\",\n );\n }\n // Re-validated rather than trusted: a snapshot is storage, and storage is\n // edited, migrated and hand-fixed.\n return serializeAttemptState(plan, {\n responses: state.responses,\n submittedSlotIds: state.submittedSlotIds,\n index: state.index,\n ...(state.savedAt !== undefined ? { savedAt: state.savedAt } : {}),\n });\n}\n\n/**\n * Reports how two snapshots' responses differ, slot by slot.\n *\n * Useful for an autosave that should only write what moved, and for an audit\n * trail that has to show what a learner changed between two saves — including\n * an answer they cleared, which a naive comparison of the later snapshot\n * alone cannot see.\n *\n * Entries come back sorted by `slotId`, so the output is stable regardless of\n * the order the two objects happened to be written in.\n */\nexport function diffResponses(\n before: Pick<AttemptState, 'responses'>,\n after: Pick<AttemptState, 'responses'>,\n): ResponseDiffEntry[] {\n const slotIds = [...new Set([...Object.keys(before.responses), ...Object.keys(after.responses)])];\n slotIds.sort();\n\n const entries: ResponseDiffEntry[] = [];\n for (const slotId of slotIds) {\n const had = Object.hasOwn(before.responses, slotId);\n const has = Object.hasOwn(after.responses, slotId);\n const from = before.responses[slotId];\n const to = after.responses[slotId];\n\n if (had && !has) {\n entries.push({ slotId, change: 'removed', ...(from !== undefined ? { before: from } : {}) });\n continue;\n }\n if (!had && has) {\n entries.push({ slotId, change: 'added', ...(to !== undefined ? { after: to } : {}) });\n continue;\n }\n // Structural comparison through the canonical form, so a response that\n // survived a JSON round-trip with its keys reordered does not read as a\n // change the learner never made.\n if (canonicalJson(from) !== canonicalJson(to)) {\n entries.push({\n slotId,\n change: 'changed',\n ...(from !== undefined ? { before: from } : {}),\n ...(to !== undefined ? { after: to } : {}),\n });\n }\n }\n return entries;\n}\n","import { ActivitySchemaError, UnknownActivityTypeError } from './errors.js';\nimport { MEDIA_FIELD_POLICY } from './registry/builtins.js';\nimport { getActivityTypeDescriptor } from './registry/index.js';\nimport { guardOf } from './registry/keep-when.js';\nimport type { FieldPolicy, Sensitivity } from './registry/registry.js';\nimport { RedactedItemGroupSchema } from './schemas/item-group.js';\nimport { readSchema } from './schemas/read-schema.js';\nimport type { ItemGroup } from './types/item-group.js';\n\n/**\n * A learner-safe projection of activity data produced by {@link redact}:\n * the activity's public fields plus the `redacted: true` marker. Renderable\n * and validatable (each built-in type registers a strict redacted schema),\n * but stripped of the answer key, scoring rules, authored feedback, and\n * author-only assets.\n */\nexport interface RedactedActivityData {\n redacted: true;\n schemaVersion: string;\n type: string;\n id: string;\n title: string;\n [key: string]: unknown;\n}\n\n/** Options for {@link redact}. */\nexport interface RedactOptions {\n /**\n * Which sensitivity tiers to keep beyond `public`:\n * - `'none'` (default) — public fields only; the output satisfies the\n * type's strict redacted schema and `assertRedacted`.\n * - `'after-submit'` — public + answer-key fields (for post-submission\n * review renders). `author-only` fields and unclassified fields are\n * STILL removed; the output will NOT pass `assertRedacted`.\n */\n reveal?: 'none' | 'after-submit';\n /**\n * Per-call sensitivity overrides, merged over the type's registered\n * `fieldPolicy` (top-level keys replace; nested objects merge one level).\n *\n * Sensitivity is partly a PEDAGOGICAL decision, not purely a security one —\n * whether a rubric or a hint is learner-visible differs legitimately between\n * deployments — and the SDK must not freeze that choice. Use this to tighten\n * a field the SDK ships as `public`:\n *\n * ```ts\n * redact(essay, { policy: { rubric: 'author-only' } });\n * ```\n *\n * Overrides can only be applied to fields; they cannot re-open a field the\n * caller has not classified, because unclassified still means removed.\n * What can be tightened is a field the type's `redactedSchema` leaves\n * OPTIONAL, as it leaves the rubric above: the schema check rejects a\n * payload that reveals more than the learner-safe shape, and equally one\n * that is missing part of it. Tightening a field the redacted shape requires\n * — a read-aloud item's `scoring`, `recording`, `referenceText` or `locale`,\n * without which the learner has no item to read — throws\n * {@link ActivitySchemaError} instead of hiding it.\n */\n policy?: FieldPolicy;\n}\n\n/** Merges per-call overrides over a registered policy, one level deep. */\nfunction mergePolicy(base: FieldPolicy, overrides: FieldPolicy | undefined): FieldPolicy {\n if (overrides === undefined) {\n return base;\n }\n const merged: Record<string, Sensitivity | FieldPolicy> = { ...base };\n for (const [key, override] of Object.entries(overrides)) {\n const current = merged[key];\n merged[key] =\n typeof override === 'object' && typeof current === 'object'\n ? { ...current, ...override }\n : override;\n }\n return merged;\n}\n\nfunction isSensitivity(value: Sensitivity | FieldPolicy): value is Sensitivity {\n return typeof value === 'string';\n}\n\nfunction keepField(sensitivity: Sensitivity, reveal: 'none' | 'after-submit'): boolean {\n if (sensitivity === 'public') {\n return true;\n }\n return sensitivity === 'answer-key' && reveal === 'after-submit';\n}\n\n/** An object projection that kept no fields at all. Arrays are never dropped. */\nfunction isEmptyObject(value: unknown): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0\n );\n}\n\nfunction redactValue(\n value: unknown,\n policy: FieldPolicy,\n reveal: 'none' | 'after-submit',\n): unknown {\n if (Array.isArray(value)) {\n return value.map((element) => redactValue(element, policy, reveal));\n }\n if (value === null || typeof value !== 'object') {\n // A nested FieldPolicy cannot classify a primitive's sub-fields; the\n // policy author classified an object shape that is not there. Fail\n // closed: drop it (handled by the caller returning undefined).\n return undefined;\n }\n\n const source = value as Record<string, unknown>;\n const output: Record<string, unknown> = {};\n for (const [key, fieldValue] of Object.entries(source)) {\n if (fieldValue === undefined) {\n continue;\n }\n const classification = policy[key];\n if (classification === undefined) {\n // Fail closed: unclassified fields are never emitted, at any reveal\n // level. Adding a field without classifying it hides it — never leaks it.\n continue;\n }\n if (isSensitivity(classification)) {\n if (keepField(classification, reveal)) {\n output[key] = fieldValue;\n }\n continue;\n }\n // A guarded policy drops a value its test refuses: the field is left out,\n // as an unclassified one is. It can only remove, never add.\n const guard = guardOf(classification);\n if (guard !== undefined && !guard(fieldValue)) {\n continue;\n }\n const nested = redactValue(fieldValue, classification, reveal);\n // A nested projection that emitted nothing is omitted rather than emitted\n // as `{}`. \"Nothing survived redaction\" and \"the field was absent\" are the\n // same thing to a learner — and without this, giving an all-`answer-key`\n // object a nested policy would turn its absence under `reveal: 'none'`\n // into an empty object, changing every existing projection.\n if (nested !== undefined && !isEmptyObject(nested)) {\n output[key] = nested;\n }\n }\n return output;\n}\n\n/**\n * Produces the learner-safe projection of activity data (R7), driven by the\n * `fieldPolicy` registered for `data.type`. Fail-closed and exhaustive by\n * construction: a field the policy does not classify is removed — including\n * unknown passthrough fields — so a NEW field added by a future SDK or a\n * consumer sidecar can never leak through an out-of-date redactor.\n *\n * With the default `reveal: 'none'`, the output validates against the type's\n * strict redacted schema (verified here; an invalid projection throws\n * {@link ActivitySchemaError} rather than shipping an unproven payload).\n *\n * @throws UnknownActivityTypeError when `data.type` is not registered.\n * @throws Error when the registered descriptor declares no `fieldPolicy`\n * (redaction cannot guess sensitivities).\n */\nexport function redact<T extends { type: string }>(\n data: T,\n options: RedactOptions = {},\n): RedactedActivityData {\n const reveal = options.reveal ?? 'none';\n const descriptor = getActivityTypeDescriptor(data.type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(data.type));\n }\n if (descriptor.fieldPolicy === undefined) {\n throw new Error(\n `Activity type \"${descriptor.type}\" has no fieldPolicy; redact() cannot run fail-closed redaction without one.`,\n );\n }\n\n const effectivePolicy = mergePolicy(descriptor.fieldPolicy, options.policy);\n const projected = redactValue(data, effectivePolicy, reveal) as Record<string, unknown>;\n const result = { ...projected, redacted: true as const } as RedactedActivityData;\n\n if (reveal === 'none' && descriptor.redactedSchema !== undefined) {\n const parsed = readSchema(\n descriptor.redactedSchema,\n result,\n `Activity type \"${descriptor.type}\"`,\n );\n if (!parsed.success) {\n throw new ActivitySchemaError(\n descriptor.type,\n parsed.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n }\n\n return result;\n}\n\n/**\n * Asserts that `data` is a learner-safe redacted projection: it carries the\n * `redacted: true` marker and, when its type registers a strict redacted\n * schema, validates against it (proving the absence of answer-key fields).\n * Use this at the server boundary before sending activity data to a client\n * that must not hold the key.\n */\nexport function assertRedacted(data: unknown): asserts data is RedactedActivityData {\n if (typeof data !== 'object' || data === null) {\n throw new ActivitySchemaError('unknown', [\n { path: [], message: 'Redacted activity data must be an object.', code: 'invalid_type' },\n ]);\n }\n const candidate = data as Record<string, unknown>;\n if (candidate.redacted !== true) {\n throw new ActivitySchemaError(String(candidate.type ?? 'unknown'), [\n {\n path: ['redacted'],\n message: 'Missing redacted marker — this payload is not a redact() projection.',\n code: 'custom',\n },\n ]);\n }\n const type = typeof candidate.type === 'string' ? candidate.type : '';\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(type);\n }\n if (descriptor.redactedSchema === undefined) {\n // Fail closed: without a registered redacted schema there is no way to\n // PROVE the absence of answer-key fields, and a marker alone proves\n // nothing (any object can carry `redacted: true`).\n throw new ActivitySchemaError(descriptor.type, [\n {\n path: [],\n message: `Activity type \"${descriptor.type}\" registers no redactedSchema; assertRedacted cannot prove this payload is learner-safe.`,\n code: 'custom',\n },\n ]);\n }\n const parsed = readSchema(\n descriptor.redactedSchema,\n candidate,\n `Activity type \"${descriptor.type}\"`,\n );\n if (!parsed.success) {\n throw new ActivitySchemaError(\n descriptor.type,\n parsed.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n}\n\n/**\n * Sensitivity of a stimulus. Everything is learner-visible — that is what a\n * stimulus IS — except the author's transcript and script. Fail-closed like every other\n * policy: a field added to `Stimulus` without a classification here is\n * dropped, never leaked.\n */\nconst STIMULUS_FIELD_POLICY: FieldPolicy = {\n id: 'public',\n kind: 'public',\n title: 'public',\n body: 'public',\n bodyHtml: 'public',\n media: MEDIA_FIELD_POLICY,\n locale: 'public',\n attribution: 'public',\n transcript: 'author-only',\n script: 'author-only',\n};\n\n/** The group container's own fields. `items` is handled separately, per item type. */\nconst ITEM_GROUP_FIELD_POLICY: FieldPolicy = {\n schemaVersion: 'public',\n type: 'public',\n id: 'public',\n title: 'public',\n // See the note on the built-in activity policies: a slot key is identity,\n // and identity has to cross the redaction boundary intact.\n slotKey: 'public',\n shuffle: 'public',\n stimulus: STIMULUS_FIELD_POLICY,\n // Public, and classified key by key rather than as one leaf: the learner's\n // player needs every part of it, none of it is a key — and a leaf would hand\n // over the author's object by reference, with whatever else was parked in it.\n timeline: {\n // Applies to every element of the array.\n cues: {\n id: 'public',\n at: 'public',\n itemIds: 'public',\n title: 'public',\n required: 'public',\n },\n chapters: {\n at: 'public',\n title: 'public',\n },\n navigation: 'public',\n },\n};\n\n/** A learner-safe item group: the container with its marker, holding `redact()` projections. */\nexport type RedactedItemGroup = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Produces the learner-safe projection of an item group: the container and\n * stimulus under their own fail-closed policy (the author transcript goes;\n * the passage, media and attribution stay — the learner is meant to see\n * them), and every item through {@link redact} with the same `options`, so\n * a per-call `policy` tightens each item exactly as it would alone.\n *\n * With the default `reveal: 'none'` the container is verified against the\n * strict redacted schema; each item was already verified by `redact`.\n */\nexport function redactItemGroup<TItem extends { type: string }>(\n group: ItemGroup<TItem>,\n options: RedactOptions = {},\n): RedactedItemGroup {\n const reveal = options.reveal ?? 'none';\n const { items, ...container } = group;\n const projected = redactValue(container, ITEM_GROUP_FIELD_POLICY, reveal) as Record<\n string,\n unknown\n >;\n const result = {\n ...projected,\n items: items.map((item) => redact(item, options)),\n redacted: true as const,\n } as RedactedItemGroup;\n\n if (reveal === 'none') {\n const parsed = RedactedItemGroupSchema.safeParse(result);\n if (!parsed.success) {\n throw new ActivitySchemaError(\n 'item-group',\n parsed.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n }\n return result;\n}\n\n/**\n * Asserts that `data` is a learner-safe item group: it carries the marker,\n * its container and stimulus satisfy the strict redacted schema (so no\n * transcript, no unclassified field), and EVERY item passes\n * {@link assertRedacted} against its own type's redacted schema. Item\n * failures are reported at `items.<index>.…`. Use it at the server boundary\n * before sending a group to a client that must not hold the key.\n */\nexport function assertRedactedItemGroup(data: unknown): asserts data is RedactedItemGroup {\n if (typeof data !== 'object' || data === null) {\n throw new ActivitySchemaError('item-group', [\n { path: [], message: 'Redacted item group must be an object.', code: 'invalid_type' },\n ]);\n }\n const candidate = data as Record<string, unknown>;\n if (candidate.redacted !== true) {\n throw new ActivitySchemaError('item-group', [\n {\n path: ['redacted'],\n message: 'Missing redacted marker — this payload is not a redactItemGroup() projection.',\n code: 'custom',\n },\n ]);\n }\n const parsed = RedactedItemGroupSchema.safeParse(candidate);\n if (!parsed.success) {\n throw new ActivitySchemaError(\n 'item-group',\n parsed.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n );\n }\n parsed.data.items.forEach((item, index) => {\n try {\n assertRedacted(item);\n } catch (error) {\n if (error instanceof ActivitySchemaError) {\n throw new ActivitySchemaError(\n 'item-group',\n error.errors.map((issue) => ({\n ...issue,\n path: ['items', String(index), ...issue.path],\n })),\n );\n }\n throw error;\n }\n });\n}\n\n/** A deep copy of plain data with every field its policy calls `author-only` left out. */\nfunction withoutAuthorOnlyValue(value: unknown, policy: FieldPolicy | undefined): unknown {\n if (Array.isArray(value)) {\n return value.map((element) => withoutAuthorOnlyValue(element, policy));\n }\n if (value === null || typeof value !== 'object') {\n return value;\n }\n const output: Record<string, unknown> = {};\n for (const [key, field] of Object.entries(value as Record<string, unknown>)) {\n const classification = policy?.[key];\n if (classification === 'author-only') {\n continue;\n }\n output[key] = withoutAuthorOnlyValue(\n field,\n classification === undefined || isSensitivity(classification) ? undefined : classification,\n );\n }\n return output;\n}\n\n/** The policy an activity is stripped under; the same one `redact()` reads. */\nfunction policyOf(type: unknown): FieldPolicy {\n const descriptor = typeof type === 'string' ? getActivityTypeDescriptor(type) : undefined;\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(type));\n }\n if (descriptor.fieldPolicy === undefined) {\n throw new Error(\n `Activity type \"${descriptor.type}\" has no fieldPolicy; withoutAuthorOnly() cannot tell which of its fields are author-only.`,\n );\n }\n return descriptor.fieldPolicy;\n}\n\n/**\n * A copy of an activity or an item group without its `author-only` fields:\n * a recording's `transcript` and `script`, on an item's media, a\n * multiple-choice option's recording, a stimulus or its media. Everything\n * else is kept as stored, the answer key and your own fields included, and\n * the copy is not marked redacted, so it scores exactly as the stored item.\n *\n * For practice an app scores in the browser, where the learner may hold the\n * key but not the author's material. It is NOT a learner-safe projection: an\n * exam, or anything a learner must not be able to answer from the payload,\n * goes through {@link redact} / {@link redactItemGroup}, whose output a scorer\n * refuses.\n *\n * ```ts\n * res.json(withoutAuthorOnly(practiceItem));\n * ```\n *\n * @throws UnknownActivityTypeError when an activity's type is not registered,\n * and Error when it declares no `fieldPolicy`.\n */\nexport function withoutAuthorOnly<T>(entry: T): T {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n throw new TypeError('withoutAuthorOnly: expects an activity or an item group.');\n }\n const record = entry as Record<string, unknown>;\n if (record.type !== 'item-group') {\n return withoutAuthorOnlyValue(entry, policyOf(record.type)) as T;\n }\n const { items, ...container } = record;\n return {\n ...(withoutAuthorOnlyValue(container, ITEM_GROUP_FIELD_POLICY) as Record<string, unknown>),\n ...(Array.isArray(items)\n ? {\n items: items.map((item) =>\n withoutAuthorOnlyValue(item, policyOf((item as { type?: unknown } | null)?.type)),\n ),\n }\n : items !== undefined\n ? { items }\n : {}),\n } as T;\n}\n","import { entryKeyOf } from './media-budget.js';\nimport {\n type AssessmentScore,\n type CompositionPolicy,\n composeAssessmentScore,\n type ScoredItem,\n} from './scoring/compose.js';\nimport type { MediaProgress } from './types/item-group.js';\n\n/**\n * Reads a stored {@link MediaProgress}. Never throws: anything that is not one\n * — a string, a stale shape, an object whose getters throw — answers `null`,\n * and the player starts from the beginning.\n *\n * What it keeps, it clamps. A stored position is host storage, which is input\n * from a stranger by the time it is read back: a negative `at`, a `furthest`\n * behind `at`, or a position past the end of a video that has since been\n * re-cut must not put the playhead somewhere it cannot be. Give `duration`\n * once the video's metadata is known, and both positions are held inside it.\n */\nexport function readMediaProgress(value: unknown, duration?: number): MediaProgress | null {\n let version: unknown;\n let at: unknown;\n let furthest: unknown;\n try {\n if (typeof value !== 'object' || value === null) {\n return null;\n }\n // Each field read once: an accessor that answered the check one value and\n // the clamp another would put back exactly what the check refused.\n ({ progressVersion: version, at, furthest } = value as Record<string, unknown>);\n } catch {\n return null;\n }\n if (version !== '1.0' || typeof at !== 'number' || !Number.isFinite(at)) {\n return null;\n }\n const end =\n typeof duration === 'number' && Number.isFinite(duration) && duration >= 0\n ? duration\n : Number.POSITIVE_INFINITY;\n const clampedAt = Math.min(Math.max(0, at), end);\n const reached = typeof furthest === 'number' && Number.isFinite(furthest) ? furthest : clampedAt;\n return {\n progressVersion: '1.0',\n at: clampedAt,\n furthest: Math.min(Math.max(clampedAt, reached), end),\n };\n}\n\n/**\n * One interactive video's composed result: the scored items whose slot belongs\n * to the entry, composed as one section of weight 1. A slot belongs when\n * `entryKeyOf(item.slotId) === entryKey`, so a host can pass a whole attempt's\n * items and get the video's share of it.\n *\n * `entryKey` is the group's `slotKey` when it has one — which every published\n * video should — or its position in the sequence otherwise. A group object\n * alone cannot know its position, which is why this takes the key.\n *\n * @throws Error when no item belongs to the entry. A mistyped key would\n * otherwise compose nothing into a `final` result, and a refusal must\n * never read as a grade.\n */\nexport function composeTimelineScore(\n entryKey: string,\n items: readonly ScoredItem[],\n policy: CompositionPolicy,\n): AssessmentScore {\n const own = items.filter((item) => entryKeyOf(item.slotId) === entryKey);\n if (own.length === 0) {\n throw new Error(\n `composeTimelineScore: no scored item belongs to entry \"${entryKey}\". Pass the video's ` +\n 'slotKey (or its position in the sequence), and the items of the attempt that holds it.',\n );\n }\n return composeAssessmentScore([{ id: entryKey, weight: 1, items: own }], policy);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { G as groupCaptionsField, W as GROUP_CAPTIONS_REVEAL_DICTATION, _t as MediaSchema, a as readSchema, t as getActivityTypeDescriptor, vt as RedactedMediaSchema, yt as maxUnits } from "./registry-Dsx9mNsS.js";
1
+ import { Ct as MediaSchema, Dt as maxUnits, Et as ScriptSchema, X as GROUP_CAPTIONS_REVEAL_DICTATION, Z as groupCaptionsField, a as readSchema, t as getActivityTypeDescriptor, wt as RedactedMediaSchema } from "./registry-B23o9UWh.js";
2
2
  import * as z from "zod/v4";
3
3
  //#region src/timeline-limits.ts
4
4
  /**
@@ -85,6 +85,7 @@ const StimulusSchema = z.looseObject({
85
85
  bodyHtml: z.string().optional(),
86
86
  media: MediaSchema.optional(),
87
87
  transcript: z.string().optional(),
88
+ script: ScriptSchema.optional(),
88
89
  locale: z.string().optional(),
89
90
  attribution: z.string().optional()
90
91
  }).refine((stimulus) => stimulus.bodyHtml === void 0 || hasBody(stimulus), {
@@ -442,4 +443,4 @@ function validateItemGroup(data) {
442
443
  //#endregion
443
444
  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
445
 
445
- //# sourceMappingURL=item-group-DXiR8nOr.js.map
446
+ //# sourceMappingURL=item-group-BbvvH_Ob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item-group-BbvvH_Ob.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, 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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"}
@@ -1,4 +1,4 @@
1
- const require_registry = require("./registry-DVi0LR_i.cjs");
1
+ const require_registry = require("./registry-Ih5n4PVZ.cjs");
2
2
  let zod_v4 = require("zod/v4");
3
3
  zod_v4 = require_registry.__toESM(zod_v4, 1);
4
4
  //#region src/timeline-limits.ts
@@ -86,6 +86,7 @@ const StimulusSchema = zod_v4.looseObject({
86
86
  bodyHtml: zod_v4.string().optional(),
87
87
  media: require_registry.MediaSchema.optional(),
88
88
  transcript: zod_v4.string().optional(),
89
+ script: require_registry.ScriptSchema.optional(),
89
90
  locale: zod_v4.string().optional(),
90
91
  attribution: zod_v4.string().optional()
91
92
  }).refine((stimulus) => stimulus.bodyHtml === void 0 || hasBody(stimulus), {
@@ -502,4 +503,4 @@ Object.defineProperty(exports, "validateItemGroup", {
502
503
  }
503
504
  });
504
505
 
505
- //# sourceMappingURL=item-group-DroFXgLb.cjs.map
506
+ //# sourceMappingURL=item-group-CQRbb8Rx.cjs.map