{"record":{"id":"b718264f8180fad7","repo":"Hmbown/CodeWhale","slug":"invalid-whale-point-asset","errorCode":null,"errorMessage":"Invalid whale point asset.","messagePattern":"Invalid whale point asset\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"pet/src/ui/pet.ts","lineNumber":280,"sourceCode":"  }\n  void persist(); if (audio) anchor = audio.currentTime - seconds + .08;\n});\nwindow.addEventListener('pagehide', () => { void persist(); });\nfunction animate(now: number) {\n  if (world && !restoring && !paused && !document.hidden) {\n    accumulator += last ? Math.min(.1, (now - last) / 1000) : 0;\n    while (accumulator >= 1 / 30 && (mode.value === 'live' || !tape.length || seconds < duration())) {\n      world.step(1 / 30, { motion: !motion.checked, sensitivity: 1 }); seconds = world.frame.timeMs / 1000; play(world.voices); accumulator -= 1 / 30;\n    }\n    if ((mode.value === 'live' || !tape.length) && seconds >= duration()) seek.max = String(Math.ceil(seconds / 30) * 30 + 30);\n    draw();\n  }\n  last = now; requestAnimationFrame(animate);\n}\ntry {\n  const response = await fetch('./whale-points.tsv'); if (!response.ok) throw new Error('Whale point asset is unavailable.');\n  points = (await response.text()).trim().split('\\n').map(row => row.trim().split(/\\s+/).map(Number) as [number, number]);\n  if (points.length !== 980 || points.some(p => p.length !== 2 || !p.every(Number.isFinite))) throw new Error('Invalid whale point asset.');\n  try {\n    const saved = await library.getHabitat();\n    if (saved) {\n      savedRevision = saved.revision; const h = saved.habitat;\n      if (h.petPersistenceVersion !== 1 || !Number.isFinite(h.seconds) || h.seconds < 0 || h.seconds > PET_MAX_SECONDS\n        || !['wild', 'demo', 'replay'].includes(h.source) || typeof h.still !== 'boolean' || typeof h.sourceName !== 'string') throw new Error('Saved habitat is invalid. Save a replay file before replacing it.');\n      expressionVersion = h.expressionVersion === undefined ? 1 : h.expressionVersion;\n      if (![1, 2].includes(expressionVersion) || h.checkpoint && (h.checkpoint.sim?.expressionVersion ?? 1) !== expressionVersion) throw new Error('Saved expression version does not match its checkpoint.');\n      const savedWorld = PetWorld.fromRecording(points, { ...h, petReplayVersion: h.petReplayVersion === undefined ? 1 : h.petReplayVersion });\n      if (h.checkpoint && savedWorld.frame.timeMs / 1000 !== h.seconds) throw new Error('Saved pet clock does not match its checkpoint.');\n      tape = h.tape; interactions = [...h.interactions]; mode.value = h.source; motion.checked = h.still || media.matches;\n      source.textContent = h.sourceName;\n      seek.max = String(Math.max(h.source === 'demo' ? 80 : h.source === 'replay' ? 90 : 120, savedWorld.endTimeMs / 1000));\n      if (h.source === 'replay') {\n        mode.querySelector<HTMLOptionElement>('[value=\"replay\"]')!.disabled = false;\n      }\n      message.textContent = 'Restoring the saved habitat…';\n      if (h.checkpoint && h.still === motion.checked) adoptWorld(savedWorld);","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/ui/pet.ts#L262-L298","documentation":"Thrown after successfully fetching `whale-points.tsv` when the parsed data does not have exactly 980 rows or any row does not contain exactly two finite numbers. This validates the whale geometry asset's strict format before building the world.","triggerScenarios":"The TSV is corrupted, truncated, has extra/missing columns, or was replaced with a file of a different point count; whitespace/encoding changes produce non-numeric values (`NaN` from `Number`).","commonSituations":"Editing or regenerating the asset with a script that changes point count, uploading a stale asset from another version, files saved with headers or comments.","solutions":["Restore the original 980-point, two-column whitespace-separated `whale-points.tsv`.","Re-export the asset from the tool that generates it and re-count rows/columns.","Strip headers/comments/blank lines and ensure every row has exactly two finite numeric values."],"exampleFix":"// before: 979-point asset\n// after: validate before shipping\nconst rows = text.trim().split('\\n');\nif (rows.length !== 980 || rows.some(r => r.trim().split(/\\s+/).length !== 2)) throw new Error('regenerate asset');","handlingStrategy":"validation","validationCode":"const rows = tsv.trim().split('\\n');\nconst ok = rows.length === 980 && rows.every(r => { const c = r.trim().split(/\\s+/); return c.length === 2 && c.every(v => Number.isFinite(Number(v))); });\nif (!ok) throw new Error('bad whale-points.tsv');","typeGuard":"const isPoint = (p: unknown[]): p is [number, number] => p.length === 2 && p.every(Number.isFinite);","tryCatchPattern":"try { points = parseWhalePoints(tsv); } catch (e) { showFatal('Whale asset is corrupt: regenerate it.'); }","preventionTips":["Validate the asset in CI (980 rows, 2 finite numbers each).","Never hand-edit the generated TSV.","Keep asset generation and consumption versions in lockstep."],"tags":["asset","validation","parse"],"backgroundTag":"schema-validation-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}