{"record":{"id":"615444720aa14edc","repo":"Hmbown/CodeWhale","slug":"invalid-pet-particle-checkpoint","errorCode":null,"errorMessage":"Invalid pet particle checkpoint.","messagePattern":"Invalid pet particle checkpoint\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/pet-native.js","lineNumber":945,"sourceCode":"            phase: this.phase, clock: this.clock, tear: this.tear, previous: this.prev, current: this.cur,\n            color: [...this.col], frame: { ...this.frame } };\n    }\n    /** Restore into a newly constructed sim. Authored body and seeded particle\n     * identity must match exactly; a checkpoint cannot replace the whale. */\n    restore(value) {\n        const c = value;\n        const inRange = (n, low, high) => Number.isFinite(n) && n >= low && n <= high;\n        if (!c || c.version !== 1 || c.expressionVersion !== undefined && ![1, 2].includes(c.expressionVersion) || (c.expressionVersion ?? 1) !== this.expressionVersion || !Array.isArray(c.body) || c.body.length !== this.p.length\n            || c.body.some((v, i) => !Array.isArray(v) || v.length !== 3 || v[0] !== this.p[i].hx || v[1] !== this.p[i].hy || v[2] !== this.p[i].s)\n            || !Array.isArray(c.particles) || c.particles.length !== this.p.length\n            || c.particles.some(v => !Array.isArray(v) || v.length !== 8 || v.some((n, i) => !inRange(n, i === 4 || i === 5 ? 0 : -8, i === 4 || i === 5 ? 2 * exports.PET_MAX_SECONDS : 8)))\n            || !inRange(c.phase, 0, exports.PET_MAX_SECONDS) || !inRange(c.clock, 0, exports.PET_MAX_SECONDS) || !inRange(c.tear, 0, 1)\n            || ![c.previous, c.current].every(n => Number.isInteger(n) && n >= 0 && n < exports.CHANNELS.length)\n            || !Array.isArray(c.color) || c.color.length !== 3 || c.color.some(n => !inRange(n, 0, 255))\n            || !c.frame || ![c.frame.r, c.frame.g, c.frame.b].every(n => inRange(n, 0, 255))\n            || !inRange(c.frame.alpha, 0, 1) || !inRange(c.frame.work, 0, 1) || typeof c.frame.hollow !== 'boolean'\n            || c.frame.channel !== exports.CHANNELS[c.current].key || c.frame.arch !== exports.CHANNELS[c.current].arch)\n            throw new Error('Invalid pet particle checkpoint.');\n        this.phase = c.phase;\n        this.clock = c.clock;\n        this.tear = c.tear;\n        this.prev = c.previous;\n        this.cur = c.current;\n        this.col = [...c.color];\n        this.frame = { ...c.frame };\n        this.p.forEach((p, i) => { [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty] = c.particles[i]; });\n    }\n    /** Advance the sim by dt seconds under `state`. Identical math on every port. */\n    step(dt, state, opts) {\n        const S = (v) => lerp(0.5, v, opts.sensitivity);\n        const act = S(state.activity), coh = S(state.coherence), att = S(state.attention);\n        const seen = S(state.observed === undefined ? 1 : state.observed);\n        const motion = opts.motion ? 1 : 0;\n        this.phase += dt * (0.18 + act * 0.55) * motion;\n        this.clock += dt * (opts.motion ? 1 : 0);\n        if (exports.CHANNEL_INDEX[state.channel] !== undefined)","sourceCodeStart":927,"sourceCodeEnd":963,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/pet_watch/pet-native.js#L927-L963","documentation":"A particle checkpoint is validated field-by-field before being applied: phase/clock/tear ranges, previous/current channel indices, 3-channel color, frame RGB/alpha/work ranges, boolean hollow, and frame channel/arch matching the current CHANNELS entry. Any violation throws 'Invalid pet particle checkpoint.' rather than rendering a corrupt pet.","triggerScenarios":"Calling the checkpoint-restore path (e.g. GyrePet.restoreCheckpoint(c) / constructor with checkpoint) where c is missing fields, has out-of-range numbers (NaN included — inRange rejects non-finite), an invalid channel index, a 2- or 4-length color array, or a frame whose channel/arch do not match CHANNELS[c.current].","commonSituations":"Restoring a checkpoint serialized by a different app version whose CHANNELS list changed (so channel/arch strings no longer match); hand-edited or truncated JSON checkpoints; NaN/undefined leaking in from a partial deserialization.","solutions":["Log the rejected checkpoint and compare each field against the constraints: phase/clock in [0, PET_MAX_SECONDS], tear in [0,1], integer channel indices < CHANNELS.length, color length 3 with 0-255 values, frame.channel/frame.arch equal to CHANNELS[current].key/.arch.","If CHANNELS was reordered or renamed since the checkpoint was saved, re-save the checkpoint with the current build or migrate the stored channel key/arch and index.","Replace undefined/NaN fields with valid defaults before restoring (e.g. color: [r,g,b], hollow: false).","If the checkpoint is from an incompatible version, start a fresh pet instead of restoring."],"exampleFix":"// before\npet.restoreCheckpoint(JSON.parse(savedJson)); // frame.arch is 'old-arch', CHANNELS now uses 'gyre'\n// after\nconst c = JSON.parse(savedJson);\nconst ch = CHANNELS[c.current];\nif (ch && c.frame && c.frame.channel === ch.key && c.frame.arch === ch.arch) {\n  pet.restoreCheckpoint(c);\n} else {\n  c.frame = { r: 0, g: 0, b: 0, alpha: 0.3, hollow: false, channel: ch.key, arch: ch.arch, work: 0 };\n  pet.restoreCheckpoint(c);\n}","handlingStrategy":"validation","validationCode":"const ok = (c, CH, MAX) => c && typeof c.phase === 'number' && typeof c.clock === 'number' &&\n  c.phase >= 0 && c.phase <= MAX && c.clock >= 0 && c.clock <= MAX && c.tear >= 0 && c.tear <= 1 &&\n  Number.isInteger(c.previous) && Number.isInteger(c.current) &&\n  c.previous >= 0 && c.current >= 0 && c.current < CH.length &&\n  Array.isArray(c.color) && c.color.length === 3 && c.color.every(n => n >= 0 && n <= 255) &&\n  !!c.frame && c.frame.channel === CH[c.current].key && c.frame.arch === CH[c.current].arch;","typeGuard":"const isParticleCheckpoint = (c, CH) => !!c && typeof c.phase === 'number' && typeof c.tear === 'number' &&\n  Number.isInteger(c.current) && c.current >= 0 && c.current < CH.length &&\n  Array.isArray(c.color) && c.color.length === 3 &&\n  !!c.frame && c.frame.channel === CH[c.current].key && c.frame.arch === CH[c.current].arch;","tryCatchPattern":"try {\n  pet.restoreCheckpoint(c);\n} catch (e) {\n  if (e.message === 'Invalid pet particle checkpoint.') resetPetToDefault();\n  else throw e;\n}","preventionTips":["Validate checkpoints at load time with the same CHANNELS table the renderer uses.","Migrate saved checkpoints when CHANNELS keys or arch values change.","Treat NaN/undefined as invalid during deserialization, before restore."],"tags":["validation","checkpoint","deserialization"],"backgroundTag":"schema-validation-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}