{"record":{"id":"d85b7d415d533f3e","repo":"moeru-ai/airi","slug":"spine-zip-must-contain-a-atlas-or-atlas-txt-fi","errorCode":null,"errorMessage":"Spine ZIP must contain a .atlas (or .atlas.txt) file","messagePattern":"Spine ZIP must contain a \\.atlas \\(or \\.atlas\\.txt\\) file","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui-spine/src/utils/spine-zip-loader.ts","lineNumber":119,"sourceCode":" * 3. Return the first matched pair as the primary layout.\n */\nexport function detectSpineLayout(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelLayout {\n  const variants = detectAllSpineLayouts(entries, atlasText)\n  if (variants.length === 0)\n    throw new Error('Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas')\n  return variants[0].layout\n}\n\n/**\n * Detects all skeleton+atlas pairs in a ZIP, returning them as named\n * variants. Useful for ZIPs containing multiple outfits/characters in\n * separate folders.\n */\nexport function detectAllSpineLayouts(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelVariant[] {\n  const allFiles = Object.keys(entries)\n  const atlasCandidates = allFiles.filter(isAtlasPath)\n  if (atlasCandidates.length === 0)\n    throw new Error('Spine ZIP must contain a .atlas (or .atlas.txt) file')\n\n  const variants: SpineModelVariant[] = []\n  const usedAtlases = new Set<string>()\n\n  // First pass: match each atlas with a same-basename skeleton.\n  for (const candidate of atlasCandidates) {\n    const baseName = stripExt(stripExt(basename(candidate)))\n    const dir = dirname(candidate)\n\n    const binaryPath = `${dir}${baseName}${SKELETON_BINARY_EXT}`\n    const jsonPath = `${dir}${baseName}${SKELETON_JSON_EXT}`\n\n    let skeletonPath: string | undefined\n    let skeletonFormat: SpineModelLayout['skeletonFormat'] = 'binary'\n\n    if (entries[binaryPath] !== undefined) {\n      skeletonPath = binaryPath\n      skeletonFormat = 'binary'","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui-spine/src/utils/spine-zip-loader.ts#L101-L137","documentation":"Thrown by detectAllSpineLayouts when no file path in the archive matches isAtlasPath — i.e. the ZIP contains no .atlas or .atlas.txt entry at all. This is the earliest structural check: without an atlas there is nothing to pair with skeletons or to derive texture pages from, so Spine loading cannot proceed.","triggerScenarios":"Uploading a ZIP with only .skel/.json + .png (atlas omitted); atlas renamed to .atlasTXT or packed into a nested inner zip; uploading a spine export's 'skeletons-only' folder; packaging that swallowed the atlas into a different archive.","commonSituations":"Users selecting the wrong folder of a Spine export; pipeline that filters 'unnecessary' text files and drops the .atlas; .atlas.txt double-extension renamed by OS tooling to .txt; asset pipelines zipping images and skeletons separately for size.","solutions":["Re-zip the complete Spine export: skeleton (.skel/.json), atlas (.atlas or .atlas.txt), and texture pages (.png)","Check nested archives — extract inner zips first if present","Scan entries for isAtlasPath before loading and surface a clear message","If the atlas genuinely has an unusual extension, rename it to .atlas.txt (contents are text) before zipping"],"exampleFix":"// before\nconst assets = await loadSpineModelFromZip(file) // throws '.atlas (or .atlas.txt) file'\n\n// after\nconst names = Object.keys(zip.files)\nif (!names.some(n => /\\.atlas(\\.txt)?$/i.test(n))) {\n  throw new Error('No .atlas file found. Include the atlas exported by Spine (hero.atlas) together with the skeleton and textures.')\n}","handlingStrategy":"validation","validationCode":"const hasAtlas = Object.keys(entries).some(n => /\\.atlas(\\.txt)?$/i.test(n))\nif (!hasAtlas) throw new Error('Spine export is missing its .atlas file')","typeGuard":"const isAtlasPath = (p: string): boolean => /\\.atlas(\\.txt)?$/i.test(p)","tryCatchPattern":"catch (e) { if (e.message.includes('.atlas (or .atlas.txt) file')) { showUserError('Include the atlas from the Spine export'); return } throw e }","preventionTips":["Zip the complete Spine export (skeleton + atlas + png pages)","Don't strip 'unused' text files during packaging","Rename odd extensions to .atlas.txt before zipping"],"tags":["spine","zip","atlas","missing-assets"],"backgroundTag":"spine-model-load-failed","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}