{"record":{"id":"7db10ea876e91989","repo":"Hmbown/CodeWhale","slug":"invalid-pet-score-checkpoint-audio","errorCode":null,"errorMessage":"Invalid pet score checkpoint.","messagePattern":"Invalid pet score checkpoint\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/pet-audio.ts","lineNumber":21,"sourceCode":"import type { WorldFrame } from './pet-world.js';\n\nexport interface PetVoice {\n  id: string; start: number; duration: number; frequency: number;\n  gain: number; pan: number; kind: 'tone' | 'noise';\n}\n\n/** Core emits score events; WebAudio / AVAudioEngine only present their PCM.\n * Calling at any display cadence produces the same score when all world ticks\n * are supplied. Calling twice for a world tick cannot retrigger a voice. */\nexport class PetScore {\n  private lastWindow = -1;\n  private lastSequence = -1;\n  private lastAddress = false;\n  checkpoint(): [number, number, boolean] { return [this.lastWindow, this.lastSequence, this.lastAddress]; }\n  restore(value: unknown): void {\n    if (!Array.isArray(value) || value.length !== 3\n      || !value.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= PET_MAX_SECONDS * 2.5) || typeof value[2] !== 'boolean')\n      throw new Error('Invalid pet score checkpoint.');\n    [this.lastWindow, this.lastSequence, this.lastAddress] = value as [number, number, boolean];\n  }\n  voices(frame: WorldFrame): PetVoice[] {\n    const time = frame.timeMs / 1000, window = Math.floor((frame.timeMs + 1e-7) / 400);\n    const out: PetVoice[] = [];\n    const add = (id: string, frequency: number, duration: number, gain: number, pan = 0, delay = 0, kind: PetVoice['kind'] = 'tone') =>\n      out.push({ id, start: time + delay, duration, frequency, gain, pan, kind });\n    const t = frame.telemetry, fresh = t !== undefined && t.sequence !== this.lastSequence;\n    if (fresh) {\n      this.lastSequence = t.sequence;\n      for (let c = 0; c < CHANNELS.length; c++) {\n        const channel = CHANNELS[c], n = t.onsets[c];\n        if (!n || channel.sustained || ['human', 'error'].includes(channel.key)) continue;\n        add(`onset:${t.sequence}:${c}`, channel.freq, .24, .035 * Math.min(2, Math.sqrt(n)), (c / 12 - .5) * .7);\n      }\n      if (t.errors) add(`tear:${t.sequence}`, CHANNELS.find(c => c.key === 'error')!.freq, .22, .05, 0, 0, 'noise');\n    }\n    const address = frame.state.channel === 'human' && frame.state.attention > .5;","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/pet-audio.ts#L3-L39","documentation":"restore() validates a deserialized checkpoint tuple before assigning it back to the audio state. A valid checkpoint is exactly [lastWindow, lastSequence, lastAddress]: an array of length 3 whose first two entries are safe integers in [-1, PET_MAX_SECONDS*2.5] and whose third entry is a boolean. Anything else (wrong shape, wrong types, out-of-range values, or a corrupted/truncated payload) is rejected so bad persisted state can never poison render state.","triggerScenarios":"Calling petAudio.restore() with: a non-array (null, object, string), an array of length != 3, non-integer or negative-below -1 or above PET_MAX_SECONDS*2.5 window/sequence values (e.g. NaN, Infinity, floats), or a non-boolean third element (e.g. 0/1 stored instead of true/false).","commonSituations":"Loading a checkpoint from localStorage/JSON that was written by an older library version with a different tuple arity; JSON round-trip coercion turning booleans into numbers or numbers into strings; a schema migration truncating or reordering the array; a hand-edited save file.","solutions":["Inspect the value passed to restore(): log or console.log it right before the call and verify it is exactly [number, number, boolean].","Ensure checkpoints are only created via checkpoint() and stored verbatim — do not reshape, sort, or spread the tuple before saving.","Clamp/sanitize legacy payloads before restoring: coerce 0/1 to boolean and clamp window/sequence into [-1, PET_MAX_SECONDS*2.5].","If a versioned persisted format changed, migrate old checkpoints (or drop them and start from a fresh checkpoint()) rather than passing them to restore().","Wrap restore() in try/catch and fall back to checkpoint() defaults on failure."],"exampleFix":"// before\npet.restore(JSON.parse(saved));\n\n// after\nconst raw = JSON.parse(saved);\nconst ck = Array.isArray(raw) && raw.length === 3\n  ? [Number(raw[0]), Number(raw[1]), Boolean(raw[2])]\n  : pet.checkpoint();\ntry { pet.restore(ck); } catch { pet.restore(pet.checkpoint()); }","handlingStrategy":"validation","validationCode":"function isValidCheckpoint(v: unknown): boolean {\n  return Array.isArray(v) && v.length === 3\n    && v.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= PET_MAX_SECONDS * 2.5)\n    && typeof v[2] === 'boolean';\n}\nif (!isValidCheckpoint(raw)) raw = pet.checkpoint();","typeGuard":"function isCheckpoint(v: unknown): v is [number, number, boolean] {\n  return Array.isArray(v) && v.length === 3\n    && Number.isSafeInteger(v[0]) && v[0] >= -1 && v[0] <= PET_MAX_SECONDS * 2.5\n    && Number.isSafeInteger(v[1]) && v[1] >= -1 && v[1] <= PET_MAX_SECONDS * 2.5\n    && typeof v[2] === 'boolean';\n}","tryCatchPattern":"try {\n  pet.restore(saved);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Invalid pet score checkpoint.') {\n    pet.restore(pet.checkpoint()); // reset to defaults\n  } else throw err;\n}","preventionTips":["Always persist the exact array returned by checkpoint(); never reshape it.","Use a type guard before restoring untrusted (localStorage/JSON) data.","Booleans survive JSON round-trips, but verify no manual 0/1 encoding is used.","Add a version field alongside stored checkpoints so schema changes trigger migration instead of restore()."],"tags":["validation","serialization","typescript"],"backgroundTag":"invalid-argument-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}