{"record":{"id":"7ead3beebb82a708","repo":"heygen-com/hyperframes","slug":"storyboard-frame-frameindex-not-found","errorCode":null,"errorMessage":"storyboard frame ${frameIndex} not found","messagePattern":"storyboard frame (.+?) not found","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/storyboard/editStoryboard.ts","lineNumber":91,"sourceCode":"/**\n * Set (or insert) a metadata field on the frame at `frameIndex` (1-based).\n * Replaces an existing `- key: …` line (matching any alias) in place; otherwise\n * inserts a new line right after the frame heading.\n *\n * Throws when the frame doesn't exist, so a stale/raced index (e.g. the frame\n * was deleted on disk after render) surfaces as an error instead of a silent\n * no-op the UI would report as a successful save.\n */\nexport function setFrameField(\n  source: string,\n  frameIndex: number,\n  key: string,\n  value: string,\n  opts: { aliases?: readonly string[]; quote?: boolean } = {},\n): string {\n  const lines = source.split(/\\r?\\n/);\n  const target = frameBounds(lines)[frameIndex - 1];\n  if (!target) throw new Error(`storyboard frame ${frameIndex} not found`);\n\n  const aliases = new Set([key, ...(opts.aliases ?? [])].map((k) => k.toLowerCase()));\n  const formatted = formatValue(value, opts.quote ?? false);\n\n  for (let i = target.start + 1; i < target.end; i++) {\n    const match = META_LINE_RE.exec(lines[i] ?? \"\");\n    if (match && aliases.has((match[2] ?? \"\").toLowerCase())) {\n      lines[i] = `${match[1]}${match[2]}${match[3]}${formatted}`;\n      return lines.join(\"\\n\");\n    }\n  }\n\n  lines.splice(target.start + 1, 0, `- ${key}: ${formatted}`);\n  return lines.join(\"\\n\");\n}\n\n/** Set the voiceover (guide) line for a frame, matching any voiceover alias. */\nexport function setFrameVoiceover(source: string, frameIndex: number, value: string): string {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/core/src/storyboard/editStoryboard.ts#L73-L109","documentation":"Thrown by setFrameField() when the 1-based frameIndex does not correspond to a frame heading in the source STORYBOARD.md. frameBounds() walks the markdown splitting headings via FRAME_HEADING_RE; if `frameBounds(lines)[frameIndex - 1]` is undefined, the requested frame does not exist in the authored storyboard.","triggerScenarios":"Calling setFrameField(source, frameIndex, key, value) with frameIndex greater than the number of frame headings, frameIndex <= 0, or frameIndex valid at authoring time but stale after the storyboard was edited/deleted on disk. Used by the storyboard frame-focus editor when persisting `voiceover`/`status` edits.","commonSituations":"UI editor holding a stale frame list after the underlying STORYBOARD.md changed in another tab/Git pull; passing an index from a 0-based caller into the 1-based API; deleting frames in the markdown while the editor session still references the old indices; concurrent edits where a collaborator removed the frame between read and write.","solutions":["Before writing, re-read the current STORYBOARD.md and re-parse frame bounds to confirm the index still exists.","Validate that `frameIndex >= 1 && frameIndex <= parsedFrameCount` before calling setFrameField.","If the frame was deleted intentionally, discard the edit rather than retrying against a stale index.","Refresh the editor's frame list from disk on focus/save to avoid stale-index writes."],"exampleFix":"// before\nconst next = setFrameField(source, frameIndex, \"status\", \"approved\");\n\n// after\nconst bounds = parseStoryboard(source); // exposes frame count\nif (frameIndex < 1 || frameIndex > bounds.frames.length) {\n  throw new Error(`frame ${frameIndex} out of range (1..${bounds.frames.length})`);\n}\nconst next = setFrameField(source, frameIndex, \"status\", \"approved\");","handlingStrategy":"validation","validationCode":"import { parseStoryboard } from \"./parseStoryboard.js\";\n\nfunction assertFrameInRange(source: string, frameIndex: number): void {\n  const { frames } = parseStoryboard(source);\n  if (!Number.isInteger(frameIndex) || frameIndex < 1 || frameIndex > frames.length) {\n    throw new RangeError(`frame index ${frameIndex} out of range (1..${frames.length})`);\n  }\n}","typeGuard":"function isValidFrameIndex(source: string, frameIndex: unknown): boolean {\n  if (typeof frameIndex !== \"number\" || !Number.isInteger(frameIndex)) return false;\n  const { frames } = parseStoryboard(source);\n  return frameIndex >= 1 && frameIndex <= frames.length;\n}","tryCatchPattern":"try {\n  return setFrameField(source, frameIndex, key, value, opts);\n} catch (e) {\n  if (/frame .* not found/.test(String(e))) {\n    const { frames } = parseStoryboard(source);\n    throw new RangeError(`frame ${frameIndex} no longer exists (have 1..${frames.length}); refresh from disk`, { cause: e });\n  }\n  throw e;\n}","preventionTips":["Re-read STORYBOARD.md and re-parse frame bounds immediately before writing.","Treat the frame list as stale on focus/save; refresh from disk.","Remember frameIndex is 1-based in setFrameField."],"tags":["storyboard","markdown","index-out-of-range","editor"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}