{"record":{"id":"2e5ffbf908b6fdf2","repo":"Hmbown/CodeWhale","slug":"this-appearance-has-invalid-colors-or-control-ranges-shared","errorCode":null,"errorMessage":"This appearance has invalid colors or control ranges.","messagePattern":"This appearance has invalid colors or control ranges\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/public/shared.html","lineNumber":34,"sourceCode":"<div class=\"habitat\"><canvas id=\"tank\" tabindex=\"0\" role=\"img\" aria-label=\"Codewhale habitat\"></canvas><article class=\"result\" id=\"result\" tabindex=\"-1\" aria-live=\"polite\" aria-hidden=\"true\" inert><h2>The work is ready.</h2><div class=\"label\">Preview result · no task was run</div><p>The whale held the space while the task was running. Now the answer comes forward, with the habitat still present behind it.</p><p>In Codewhale, this space will show the completed assistant response from your existing conversation. Your draft, history, and selection stay intact.</p><button id=\"replay\">Replay transition</button></article><div class=\"activity\" id=\"activity\" aria-live=\"polite\" aria-atomic=\"true\"><h2 id=\"activity-title\">A moment between tasks.</h2><div class=\"eyebrow\" id=\"activity-evidence\">Local companion</div><div class=\"detail\" id=\"activity-detail\">Activity unobserved</div><span class=\"parallel\" id=\"activity-parallel\"></span></div><div class=\"caption\"><strong id=\"caption\">Connecting…</strong><small id=\"clock\"></small></div></div>\n<div class=\"gallery\" id=\"gallery\" aria-label=\"The same whale across eight appearances\"></div>\n<div class=\"controls\"><button id=\"focus\">Focus</button><button id=\"pulse\">Pulse</button><button id=\"sound\" aria-pressed=\"false\">Sound off</button><label><input id=\"still\" type=\"checkbox\"> Still</label><button id=\"expand\">Full habitat</button><button id=\"work\">Preview work → result</button><button id=\"finish\">Finish preview now</button><button id=\"return-live\" hidden>Return to live pet</button><span class=\"work-label\" id=\"work-label\">Escape returns</span></div><div class=\"identity\" id=\"identity\"></div></section>\n<aside class=\"settings\" aria-label=\"Appearance settings\"><h2>Behavior</h2><label class=\"action-field\">Try a preview action<select id=\"action-preview\"><option value=\"thinking\">Thinking</option><option value=\"reading\">Reading files</option><option value=\"searching\">Searching</option><option value=\"editing\">Editing files</option><option value=\"executing\">Running a command</option><option value=\"testing\">Running tests</option><option value=\"browsing\">Using the browser</option><option value=\"memory\">Retrieving context</option><option value=\"delegating\">Three parallel agents</option><option value=\"waiting\">Waiting for you</option><option value=\"responding\">Writing a response</option><option value=\"error\">An operation failed</option><option value=\"unknown\">No telemetry</option></select></label><p class=\"behavior-note\">Preview actions are simulated. Motion follows a seed; your Focus and Pulse interactions become part of the replay.</p><h2>Appearance</h2><p>Presets are starting points. Every color is yours to change.</p><div class=\"presets\" id=\"presets\"></div>\n<label class=\"field\">Background <input type=\"color\" id=\"background\" value=\"#080f15\"></label><label class=\"field\">Upper light <input type=\"color\" id=\"backgroundTop\" value=\"#182d38\"></label><label class=\"field\">Particle color <input type=\"color\" id=\"particle\" value=\"#87dddb\"></label><label class=\"field\"><span>Colors follow activity</span><input type=\"checkbox\" id=\"eventColors\" checked></label>\n<label class=\"field range\"><span>Brightness <output id=\"brightness-value\"></output></span><input type=\"range\" id=\"brightness\" min=\"0.25\" max=\"2\" step=\"0.05\" value=\"1.25\"></label><label class=\"field range\"><span>Dot size <output id=\"dotScale-value\"></output></span><input type=\"range\" id=\"dotScale\" min=\"0.65\" max=\"1.8\" step=\"0.05\" value=\"1\"></label><label class=\"field range\"><span>Glow <output id=\"glow-value\"></output></span><input type=\"range\" id=\"glow\" min=\"0\" max=\"1\" step=\"0.05\" value=\"0.5\"></label><label class=\"field\"><span>Water and horizon</span><input type=\"checkbox\" id=\"environment\" checked></label>\n<div class=\"config-actions\"><button id=\"export-config\">Save appearance</button><button id=\"import-config\">Load appearance</button><input id=\"config-file\" type=\"file\" accept=\"application/json,.json\" hidden></div><p class=\"status\" id=\"message\" role=\"status\">Opening the habitat…</p><div class=\"connection\"><button id=\"save\">Save pet replay</button><br><a href=\"pet.html\" id=\"join\">Join a live pet from the standalone viewer</a></div></aside></main>\n<script src=\"pet-native.js\"></script>\n<script>\n'use strict';\nconst $=id=>document.getElementById(id), canvas=$('tank');\nconst rgb=h=>h.match(/[a-f\\d]{2}/gi).map(n=>parseInt(n,16)),hex=c=>'#'+c.map(n=>n.toString(16).padStart(2,'0')).join('');\nconst base={background:[8,15,21],backgroundTop:[24,45,56],particle:[135,221,219],eventColors:true,brightness:1.25,dotScale:1,glow:.5,environment:true};\nconst presets=[['Ocean','#080f15','#182d38','#87dddb'],['Chalk','#e8edee','#fbfdfb','#285967'],['Graphite','#141518','#28292e','#d9e0e3'],['Linen','#e9e1d2','#f8f2e7','#79573a'],['Forest','#0c1916','#20382d','#c5db9b'],['Plum','#201a29','#3b2e4b','#e7b8d0'],['Ember','#211610','#49291b','#edb177'],['Cobalt','#101934','#263b60','#b2d7f2']].map(([name,b,t,p])=>({name,...base,background:rgb(b),backgroundTop:rgb(t),particle:rgb(p),eventColors:false,brightness:name==='Chalk'||name==='Linen'?1.8:1.35}));\nlet appearance={...base}, selected='Ocean', current, previous, lastTickAt=0, received=0, client=crypto.randomUUID(), seq=0, pending=null, nextAppearance=null, busy=false, sound=false;\nlet preview=new URLSearchParams(location.search).has('preview'), pet, points, demoTick=0, demoRunning=false, demoDone=false, actionChoice='thinking', actionTick=0, lastDraw=0, accumulator=0, galleryTick=0, previewSaved=false;\nconst media=matchMedia('(prefers-reduced-motion: reduce)');$('still').checked=media.matches;media.onchange=()=>{$('still').checked=media.matches};\nfunction valid(a){return a&&Object.keys(base).every(k=>k in a)&&['background','backgroundTop','particle'].every(k=>Array.isArray(a[k])&&a[k].length===3&&a[k].every(n=>Number.isInteger(n)&&n>=0&&n<=255))&&typeof a.eventColors==='boolean'&&typeof a.environment==='boolean'&&[['brightness',.25,2],['dotScale',.65,1.8],['glow',0,1]].every(([k,lo,hi])=>Number.isFinite(a[k])&&a[k]>=lo&&a[k]<=hi)}\nfunction clean(a){if(!valid(a))throw Error('This appearance has invalid colors or control ranges.');return Object.fromEntries(Object.keys(base).map(k=>[k,a[k]]))}\ntry{const saved=JSON.parse(localStorage.getItem('codewhale-pet-appearance-v1'));if(valid(saved))appearance=clean(saved)}catch{}\nfunction presetName(a){return presets.find(p=>JSON.stringify(clean(p))===JSON.stringify(clean(a)))?.name||'Custom'}\nselected=presetName(appearance);\nfunction syncControls(){for(const k of ['background','backgroundTop','particle'])$(k).value=hex(appearance[k]);for(const k of ['eventColors','environment'])$(k).checked=appearance[k];for(const k of ['brightness','dotScale','glow']){$(k).value=appearance[k];$(k+'-value').value=k==='glow'?Math.round(appearance[k]*100)+'%':appearance[k].toFixed(2)+'×'}for(const b of $('presets').children)b.setAttribute('aria-pressed',String(b.textContent===selected));}\nfunction saveAppearance(){try{localStorage.setItem('codewhale-pet-appearance-v1',JSON.stringify(appearance))}catch{}if(preview){$('message').textContent='Appearance saved in this preview. Export it to keep a portable copy.'}else{nextAppearance=clean(appearance);$('message').textContent='Saving appearance to the companion…';void flushAction()}}\nfor(const a of presets){const b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');const sw=document.createElement('span');sw.className='swatch';sw.style.background=hex(a.background);const dot=document.createElement('i');dot.style.background=hex(a.particle);sw.append(dot);b.append(sw,document.createTextNode(a.name));b.onclick=()=>{appearance=clean(a);selected=a.name;syncControls();saveAppearance()};$('presets').append(b);const f=document.createElement('figure'),c=document.createElement('canvas'),label=document.createElement('figcaption');c.setAttribute('role','img');c.setAttribute('aria-label',a.name+' appearance');label.textContent=a.name;label.style.color=readable(a.background);f.append(c,label);$('gallery').append(f)}\nfor(const key of Object.keys(base))$(key).oninput=()=>{appearance[key]=['background','backgroundTop','particle'].includes(key)?rgb($(key).value):['eventColors','environment'].includes(key)?$(key).checked:Number($(key).value);if(key==='particle')appearance.eventColors=false;selected='Custom';syncControls();saveAppearance()};\nsyncControls();\nasync function request(path,body,headers={}){const r=await fetch(path,{method:body===undefined?'GET':'POST',headers:{'Content-Type':'application/json',...headers},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(2500)});if(!r.ok){let e;try{e=await r.json()}catch{}const error=Error(e?.error||'Local pet unavailable. Reopen from Codewhale or use the preview.');error.rejected=r.status===409&&!error.message.includes('storage');throw error}return r.json()}\nasync function flushAction(){if(busy||!current||preview)return;if(!pending&&nextAppearance){pending={identity:current.identity,client,seq:seq+1,source_revision:current.sourceRevision,action:{kind:'appearance',appearance:nextAppearance}};nextAppearance=null}if(!pending)return;busy=true;try{await request('/v1/action',pending);seq=pending.seq;pending=null;$('message').textContent='Appearance and interactions saved by the shared companion.'}catch(e){if(e.rejected)pending=null;$('message').textContent=e.message}finally{busy=false}}\nasync function poll(){if(preview)return;try{const frame=await request('/v1/frame');if(frame.version!==1||!Array.isArray(frame.points)||frame.points.length!==980)throw Error('Unsupported pet snapshot');if(current?.epoch!==frame.epoch){previous=null;lastTickAt=performance.now()}else if(frame.tick!==current.tick){previous=current;lastTickAt=performance.now()}current=frame;received=performance.now();if(!nextAppearance&&!pending&&valid(frame.appearance)&&JSON.stringify(appearance)!==JSON.stringify(frame.appearance)){appearance=clean(frame.appearance);selected=presetName(appearance);syncControls()}$('identity').textContent='Pet '+frame.identity+' · '+frame.source+' · tick '+frame.tick+' · '+frame.digest;if(frame.audioUnavailable&&sound){sound=false;$('sound').textContent='Sound unavailable';$('sound').setAttribute('aria-pressed','false')}if(pending||nextAppearance)await flushAction()}catch(e){$('message').textContent=e.message}finally{if(!preview)setTimeout(poll,33)}}\nfunction interact(food){if(preview){pet?.interact(food?'food':'attention',.2,-.15);return}if(!current||performance.now()-lastTickAt>800||pending)return;pending={identity:current.identity,client,seq:seq+1,source_revision:current.sourceRevision,action:{kind:'interact',food,x:.2,y:-.15}};void flushAction()}\n$('focus').onclick=()=>interact(false);$('pulse').onclick=()=>interact(true);canvas.onpointerdown=()=>interact(false);\n$('sound').onclick=async()=>{if(preview)return;try{const r=await request('/v1/audio',{client,enabled:!sound});sound=r.granted;$('sound').textContent=sound?'Sound on':'Sound off';$('sound').setAttribute('aria-pressed',String(sound))}catch(e){$('message').textContent=e.message}};\nsetInterval(()=>{if(sound&&!document.hidden&&performance.now()-lastTickAt<500)request('/v1/audio',{client,enabled:true}).catch(()=>{sound=false})},500);\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden&&sound){sound=false;void request('/v1/audio',{client,enabled:false});$('sound').textContent='Sound off';$('sound').setAttribute('aria-pressed','false')}lastDraw=0;accumulator=0});\n$('compare').onclick=()=>{document.body.classList.toggle('gallery-mode');$('compare').setAttribute('aria-pressed',String(document.body.classList.contains('gallery-mode')))};\n$('configure').onclick=()=>{document.body.classList.toggle('no-settings');$('configure').setAttribute('aria-pressed',String(!document.body.classList.contains('no-settings')))};","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/public/shared.html#L16-L52","documentation":"Thrown by `clean()` in the shared companion page when an appearance object fails the `valid()` schema check. `valid()` requires every base key to be present, background/backgroundTop/particle to be 3-element arrays of integers 0-255, eventColors and environment to be booleans, and brightness/dotScale/glow to be finite numbers within their ranges (0.25-2, 0.65-1.8, 0-1). `clean()` is the gate for saved localStorage, presets, frame-synced remote appearance, and imports.","triggerScenarios":"Loading a saved appearance from localStorage that fails `valid()`; a `/v1/frame` payload whose `frame.appearance` passes `valid()` but is re-checked by `clean()`; clicking a preset or adjusting a control that produces an out-of-range value; importing an appearance file whose `appearance` object is malformed.","commonSituations":"Hand-editing localStorage key `codewhale-pet-appearance-v1`; a corrupt or truncated saved appearance; a companion server sending appearance values of the wrong type or range; manual slider/DOM manipulation producing NaN.","solutions":["Clear the saved appearance: `localStorage.removeItem('codewhale-pet-appearance-v1')` and reload to fall back to the Ocean default.","Fix the offending field: colors must be integer arrays of 3 values 0-255; brightness 0.25-2; dotScale 0.65-1.8; glow 0-1; eventColors/environment booleans.","Re-import a freshly exported `codewhale-appearance.json` from a working instance.","If the companion server sends bad appearance data, fix or update the server so `frame.appearance` matches the schema."],"exampleFix":"// before (saved appearance)\n{ \"brightness\": 3.5, \"particle\": \"#87dddb\", ... }\n// after\n{ \"background\": [8,15,21], \"backgroundTop\": [24,45,56], \"particle\": [135,221,219], \"eventColors\": true, \"brightness\": 1.25, \"dotScale\": 1, \"glow\": 0.5, \"environment\": true }","handlingStrategy":"type-guard","validationCode":"function validAppearance(a) {\n  const base = ['background','backgroundTop','particle','eventColors','brightness','dotScale','glow','environment'];\n  return a && base.every(k => k in a) &&\n    ['background','backgroundTop','particle'].every(k => Array.isArray(a[k]) && a[k].length === 3 && a[k].every(n => Number.isInteger(n) && n >= 0 && n <= 255)) &&\n    typeof a.eventColors === 'boolean' && typeof a.environment === 'boolean' &&\n    a.brightness >= .25 && a.brightness <= 2 && a.dotScale >= .65 && a.dotScale <= 1.8 && a.glow >= 0 && a.glow <= 1;\n}","typeGuard":"const isAppearance = (a) => validAppearance(a); // narrows unknown -> appearance object before clean()","tryCatchPattern":"try {\n  const appearance = clean(raw);\n  apply(appearance);\n} catch (e) {\n  if (e.message.includes('invalid colors')) {\n    localStorage.removeItem('codewhale-pet-appearance-v1');\n    apply(DEFAULT_APPEARANCE);\n  }\n}","preventionTips":["Do not hand-edit localStorage key codewhale-pet-appearance-v1.","Export/import appearance only through the built-in buttons.","Keep brightness within 0.25-2, dotScale 0.65-1.8, glow 0-1 when writing custom files.","Reset stored appearance when the page reports validation errors."],"tags":["validation","schema","config","javascript"],"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-22T11:17:16.035Z"}