{"record":{"id":"699801099ebadf89","repo":"moeru-ai/airi","slug":"the-motion-project-overlay-points-are-not-in-time","errorCode":null,"errorMessage":"The motion project overlay points are not in time order.","messagePattern":"The motion project overlay points are not in time order\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/features/devtools/motion/live2d/composables/keyframes.ts","lineNumber":472,"sourceCode":"  const project = result.output\n  if (project.source.durationMs !== project.durationMs)\n    throw new Error('The motion project source is invalid.')\n\n  if (project.source.samples[0].atMs !== 0 || project.source.samples.at(-1)!.atMs > project.durationMs)\n    throw new Error('The motion project source timeline is invalid.')\n  for (let index = 1; index < project.source.samples.length; index++) {\n    if (project.source.samples[index].atMs < project.source.samples[index - 1].atMs)\n      throw new Error('The motion project source samples are not in time order.')\n  }\n\n  for (const overlay of project.overlays) {\n    if (overlay.endMs > project.durationMs || overlay.startMs > overlay.endMs)\n      throw new Error('The motion project contains an invalid overlay span.')\n    if (overlay.points.some(point => point.atMs < overlay.startMs || point.atMs > overlay.endMs))\n      throw new Error('The motion project contains an invalid overlay point.')\n    for (let index = 1; index < overlay.points.length; index++) {\n      if (overlay.points[index].atMs < overlay.points[index - 1].atMs)\n        throw new Error('The motion project overlay points are not in time order.')\n    }\n  }\n  return structuredClone(project)\n}\n","sourceCodeStart":454,"sourceCodeEnd":477,"githubUrl":"https://github.com/moeru-ai/airi/blob/9c213115f8bd0fff9e6eabab02b077ac32da21be/packages/stage-ui/src/features/devtools/motion/live2d/composables/keyframes.ts#L454-L477","documentation":"parseLive2DMotionProject requires overlay points to be sorted by atMs in non-decreasing order, matching the same invariant enforced on source samples. This error is thrown when consecutive overlay points have decreasing atMs values.","triggerScenarios":"Calling parseLive2DMotionProject where an overlay's points array has atMs[i] < atMs[i-1], e.g. points inserted at the front, interleaved from multiple sources, or manually reordered.","commonSituations":"UI drag interactions that move a point earlier than its predecessor; merging keyframes from two recording passes; hand-edited JSON with reordered entries.","solutions":["Sort overlay.points by atMs ascending before calling parseLive2DMotionProject","Fix the overlay editing code to keep points ordered on insert/move","Reject or re-sort imported point data during ingest"],"exampleFix":"// before\nparseLive2DMotionProject(project)\n// after\nproject.overlays.forEach(o => o.points.sort((a, b) => a.atMs - b.atMs))\nparseLive2DMotionProject(project)","handlingStrategy":"validation","validationCode":"project.overlays.forEach(o => o.points.sort((a, b) => a.atMs - b.atMs))","typeGuard":"const isSortedByAtMs = (items) => items.every((p, i) => i === 0 || p.atMs >= items[i - 1].atMs)","tryCatchPattern":"try {\n  parseLive2DMotionProject(project)\n}\ncatch (error) {\n  if (error.message.includes('overlay points are not in time order')) {\n    project.overlays.forEach(o => o.points.sort((a, b) => a.atMs - b.atMs))\n    parseLive2DMotionProject(project)\n  }\n  else throw error\n}","preventionTips":["Sort overlay points after any insert or drag-move operation","Use an insertion helper that places points by atMs instead of push","Add regression tests for point reordering flows"],"tags":["validation","live2d","motion","data-order"],"backgroundTag":"samples-not-in-time-order","analyzedSha":"9c213115f8bd0fff9e6eabab02b077ac32da21be","analyzedAt":"2026-09-02T04:27:24.639Z","contentChangedAt":"2026-09-02T04:27:24.639Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}