{"record":{"id":"2d615fb3db9181a7","repo":"tinyhumansai/openhuman","slug":"mascot-manifest-no-renderable-mascots","errorCode":null,"errorMessage":"mascot manifest: no renderable mascots","messagePattern":"mascot manifest: no renderable mascots","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/features/human/Mascot/manifest/manifestService.ts","lineNumber":116,"sourceCode":"  if (!Array.isArray(m.files) || !m.files.every(isManifestFile)) return false;\n  // A renderable mascot needs a runtime `.riv`; drop entries that only ship a\n  // source `.rev` so callers never select an unplayable asset.\n  return !!runtimeFile(m as MascotManifestEntry);\n}\n\n/**\n * Validate and normalise a parsed manifest document. Throws on a\n * fundamentally malformed payload; silently drops individual malformed\n * mascot entries (a single bad entry should never blank the whole library).\n */\nexport function parseManifest(raw: unknown): MascotManifest {\n  const doc = raw as Partial<MascotManifest> | null;\n  if (!doc || !Array.isArray(doc.mascots)) {\n    throw new Error('mascot manifest: missing mascots array');\n  }\n  const mascots = doc.mascots.filter(isManifestEntry);\n  if (mascots.length === 0) {\n    throw new Error('mascot manifest: no renderable mascots');\n  }\n  return {\n    schemaVersion: typeof doc.schemaVersion === 'number' ? doc.schemaVersion : 1,\n    generatedAt: isNonEmptyString(doc.generatedAt) ? doc.generatedAt : '',\n    mascots,\n    source: {\n      repository: doc.source?.repository ?? '',\n      branch: doc.source?.branch ?? '',\n      commit: doc.source?.commit ?? '',\n    },\n  };\n}\n\nfunction readSnapshot(): MascotManifest | null {\n  try {\n    const raw = window.localStorage.getItem(SNAPSHOT_KEY);\n    if (!raw) return null;\n    return parseManifest(JSON.parse(raw));","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/features/human/Mascot/manifest/manifestService.ts#L98-L134","documentation":"Thrown by parseManifest() when the fetched document has a mascots array but every entry fails isManifestEntry() filtering. Per the doc comment the function silently drops individual malformed entries (one bad entry should never blank the library), so reaching zero renderable entries means the payload is structurally present but entirely non-playable — e.g. every entry is missing its runtime .riv file reference.","triggerScenarios":"A manifest whose mascots array is populated (length > 0) but for which isManifestEntry() returns false for all items — missing required id/name fields, or runtimeFile(entry) resolves to nothing so no asset is playable. Common when a generation script runs before any .riv assets were committed, or when the .rev/runtime file check fails for every entry due to a renamed asset directory.","commonSituations":"Upstream mascot repo emptied or renamed its assets directory while the manifest still lists entries; manifest generated against a different schema version where entries pass the raw-shape check but fail the runtime-file check; partial deploy where JSON updated but binaries did not.","solutions":["Inspect the fetched manifest and run the same isManifestEntry() predicate per entry locally to see which check fails (fields vs runtime file).","If assets moved, fix the manifest generator or the entry paths so runtimeFile() resolves for at least one mascot.","If upstream is genuinely empty, pin MASCOT_MANIFEST_URL to the last known-good manifest revision (e.g. a tagged commit path).","Catch and fall back to the persisted snapshot or a bundled manifest so the mascot UI degrades gracefully."],"exampleFix":"// before\nconst manifest = parseManifest(await res.json());\n\n// after\nlet manifest: MascotManifest;\ntry {\n  manifest = parseManifest(await res.json());\n} catch {\n  const snapshot = readSnapshot();\n  if (!snapshot) throw err;\n  manifest = snapshot;\n}","handlingStrategy":"fallback","validationCode":"const doc = raw as { mascots?: unknown[] };\nconst renderable = (doc.mascots ?? []).filter(isManifestEntry);\nif (renderable.length === 0) { /* fall back to snapshot */ }","typeGuard":null,"tryCatchPattern":"try {\n  return parseManifest(raw);\n} catch (err) {\n  const snapshot = readSnapshot();\n  if (snapshot) return snapshot;\n  throw err;\n}","preventionTips":["Generate the manifest only after assets are committed and verified present.","Run isManifestEntry over generated entries in CI to catch all-filtered payloads before publish.","Version the manifest schema and regenerate atomically with the asset deploy."],"tags":["mascot","manifest","validation","empty-results"],"backgroundTag":"empty-api-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}