{"record":{"id":"89427f0d5a19c81d","repo":"moeru-ai/airi","slug":"the-motion-samples-must-be-in-time-order","errorCode":null,"errorMessage":"The motion samples must be in time order.","messagePattern":"The motion samples must be in time order\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/features/devtools/motion/live2d/composables/recording.ts","lineNumber":100,"sourceCode":"  let input: unknown\n  try {\n    input = JSON.parse(raw)\n  }\n  catch {\n    throw new Error('The file does not contain valid JSON.')\n  }\n\n  const result = safeParse(live2dMotionRecordingSchema, input)\n  if (!result.success)\n    throw new Error('The file is not an AIRI Live2D motion recording.')\n\n  const { durationMs, samples } = result.output\n  if (samples[0].atMs !== 0)\n    throw new Error('The first motion sample must start at 0 ms.')\n\n  for (let index = 1; index < samples.length; index++) {\n    if (samples[index].atMs < samples[index - 1].atMs)\n      throw new Error('The motion samples must be in time order.')\n  }\n\n  if (samples.at(-1)!.atMs > durationMs)\n    throw new Error('A motion sample occurs after the recording duration.')\n\n  return result.output\n}\n\n/**\n * Serializes a Live2D joystick recording as a readable JSON file.\n *\n * @example\n * stringifyLive2DMotionRecording({ format: 'airi-live2d-motion/v6', ... })\n * // => readable JSON ending with a newline\n */\nexport function stringifyLive2DMotionRecording(recording: ReadonlyLive2DMotionRecording): string {\n  return `${JSON.stringify(recording, null, 2)}\\n`\n}","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/moeru-ai/airi/blob/9c213115f8bd0fff9e6eabab02b077ac32da21be/packages/stage-ui/src/features/devtools/motion/live2d/composables/recording.ts#L82-L118","documentation":"Like the source samples of a motion project, recording samples must be non-decreasing in atMs so the interpolator can locate the surrounding pair for any time. parseLive2DMotionRecording throws this error when any sample's atMs is less than its predecessor's.","triggerScenarios":"Calling parseLive2DMotionRecording on a recording whose samples array has a decreasing atMs somewhere after the first element, from out-of-order appends, merges, or edits.","commonSituations":"Merging multiple recordings or input streams without sorting; editor drag operations that move a sample earlier without reordering the array; hand-edited JSON.","solutions":["Sort samples by atMs ascending before parsing","Fix the recording pipeline so samples are appended in chronological order","Re-record the motion if the source data is genuinely corrupted"],"exampleFix":"// before\nparseLive2DMotionRecording(raw)\n// after\nconst data = JSON.parse(raw)\ndata.samples.sort((a, b) => a.atMs - b.atMs)\nparseLive2DMotionRecording(JSON.stringify(data))","handlingStrategy":"validation","validationCode":"const data = JSON.parse(raw)\ndata.samples.sort((a, b) => a.atMs - b.atMs)\nraw = JSON.stringify(data)","typeGuard":"const isSortedByAtMs = (samples) => samples.every((s, i) => i === 0 || s.atMs >= samples[i - 1].atMs)","tryCatchPattern":"try {\n  const recording = parseLive2DMotionRecording(raw)\n}\ncatch (error) {\n  if (error.message === 'The motion samples must be in time order.') {\n    const data = JSON.parse(raw)\n    data.samples.sort((a, b) => a.atMs - b.atMs)\n    raw = JSON.stringify(data)\n    return parseLive2DMotionRecording(raw)\n  }\n  throw error\n}","preventionTips":["Sort samples after merging recordings or input streams","Keep recording appends chronological in the capture pipeline","Avoid manual reordering of sample arrays in editors"],"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"}