{"record":{"id":"3b25977c010a3405","repo":"Hmbown/CodeWhale","slug":"invalid-native-whale-body-3b2597","errorCode":null,"errorMessage":"Invalid native whale body.","messagePattern":"Invalid native whale body\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/pet-native.ts","lineNumber":19,"sourceCode":"import { PetWorld, type PetInteraction, type PetSegment } from './pet-world.js';\nimport { compilePetTelemetry, decodePetJSONL, PetLiveTape } from './pet-telemetry.js';\nimport { ARCH_OF, digest, layout, PetSim } from './pet-sim.js';\nimport { renderPetPCM, type PetVoice } from './pet-audio.js';\nimport { PetEngineTelemetry } from './pet-engine.js';\n\n/** Synchronous native boundary: JSON state and directly transferable PCM.\n * Native hosts share the actual world / score implementation, not a rewrite. */\nexport class PetNative {\n  private world: PetWorld;\n  private engine = new PetEngineTelemetry();\n  private engineTick = 0;\n  private segment?: PetSegment;\n  private liveTape = new PetLiveTape();\n  private stillProjection?: { key: string; points: number[][]; style: PetSim['frame'] };\n  constructor(pointsJSON: string, tapeJSONL = '', interactionsJSON = '[]', live = false, expressionVersion: 1 | 2 = 2) {\n    const points = JSON.parse(pointsJSON) as [number, number][];\n    if (!Array.isArray(points) || points.length !== 980 || points.some(p => !Array.isArray(p) || p.length !== 2 || !p.every(n => Number.isFinite(n) && Math.abs(n) <= 1)))\n      throw new Error('Invalid native whale body.');\n    this.world = new PetWorld(points, live ? compilePetTelemetry([]) : decodePetJSONL(tapeJSONL), JSON.parse(interactionsJSON) as PetInteraction[], expressionVersion, true);\n  }\n  step(dt: number, motion: boolean): string { this.world.step(dt, { motion, sensitivity: 1 }); return this.snapshot(); }\n  snapshot(): string { return JSON.stringify({ ...this.world.frame, voices: this.world.voices, digest: digest(this.world.sim) }); }\n  /** View-only projection. Display cadence and accessibility preferences never\n   * advance the owner, consume randomness, or change its score/checkpoint. */\n  presentation(): string {\n    const { sim, frame } = this.world;\n    const state = { ...frame.state, roamX: 0, roamY: 0, flip: 1, lit: frame.behaviour === 'doze' ? .18 : 1 };\n    const key = JSON.stringify([state, frame.pod]);\n    if (this.stillProjection?.key !== key) {\n      const still = new PetSim(sim.p.map(p => [p.hx, p.hy]), 0xC0FFEE, sim.expressionVersion);\n      const peers = frame.pod.filter(p => p.present);\n      still.step(1 / 30, state, { motion: false, sensitivity: 1,\n        podSlots: peers.length >= 3 ? peers.map(p => [[0, 2, 4, 1, 3, 5][p.slot], p.phase] as const) : undefined });\n      this.stillProjection = { key, points: still.p.map(p => [p.x, p.y]), style: still.frame };\n    }\n    return JSON.stringify({ ...frame, digest: digest(sim), style: sim.frame, activity: this.engine.activity(frame.timeMs),","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/pet-native.ts#L1-L37","documentation":"The PetNative constructor validates the whale body points JSON: it must parse to an array of exactly 980 [x,y] pairs where each coordinate is a finite number with |n| <= 1. Anything else (wrong count, non-array, non-finite or out-of-range coordinates) throws 'Invalid native whale body.' The 980-point mesh is the pet renderer's fixed body topology.","triggerScenarios":"new PetNative(pointsJSON, ...) where pointsJSON parses to an array of length != 980, contains nested arrays of length != 2, or contains NaN/Infinity/coordinates outside [-1,1]. Also thrown when pointsJSON is not valid JSON at all via JSON.parse (SyntaxError) — this specific error fires after successful parse.","commonSituations":"Using a body file saved by a different pet version with a different point count; truncated or hand-edited points JSON; units confusion (passing pixel coords 0-800 instead of normalized -1..1); passing null/undefined stringified as 'null'.","solutions":["Regenerate the whale body so it contains exactly 980 [x,y] points with coordinates normalized to [-1,1]","Validate the parsed points array (length 980, pairs of finite numbers within ±1) before constructing PetNative","Load the body from the bundled/default asset rather than a custom file","If coordinates are pixel-based, normalize: x' = (x / width) * 2 - 1, y' = (y / height) * 2 - 1"],"exampleFix":"// before\nconst pet = new PetNative(fs.readFileSync('body.json','utf8'));\n// after\nconst pts = JSON.parse(fs.readFileSync('body.json','utf8'));\nconst ok = Array.isArray(pts) && pts.length === 980 && pts.every(p => Array.isArray(p) && p.length === 2 && p.every(n => Number.isFinite(n) && Math.abs(n) <= 1));\nif (!ok) throw new Error('body.json must be 980 [x,y] points in [-1,1]');\nconst pet = new PetNative(JSON.stringify(pts));","handlingStrategy":"validation","validationCode":"function validWhaleBody(json) {\n  let pts;\n  try { pts = JSON.parse(json); } catch { return false; }\n  return Array.isArray(pts) && pts.length === 980 &&\n    pts.every(p => Array.isArray(p) && p.length === 2 && p.every(n => Number.isFinite(n) && Math.abs(n) <= 1));\n}","typeGuard":"type Pt = [number, number];\nconst isPt = (p: unknown): p is Pt =>\n  Array.isArray(p) && p.length === 2 && p.every(n => typeof n === 'number' && Number.isFinite(n) && Math.abs(n) <= 1);\nconst isWhaleBody = (v: unknown): v is Pt[] => Array.isArray(v) && v.length === 980 && v.every(isPt);","tryCatchPattern":"try {\n  pet = new PetNative(pointsJSON);\n} catch (err) {\n  if (err.message === 'Invalid native whale body.') pet = new PetNative(DEFAULT_BODY_JSON);\n  else throw err;\n}","preventionTips":["Ship a checksummed default body asset and validate custom bodies at load time","Always normalize coordinates to [-1,1] when exporting meshes","Pin the body asset format to the pet library version"],"tags":["validation","geometry","constructor"],"backgroundTag":"invalid-argument-value","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"}