← back to Dw Yolo Loop

scripts/image-alt-audit/image-alt-audit.mjs

96 lines

// image-alt-audit — READ-ONLY, $0. Cycle 66 (DTD-picked B, unanimous).
// After 4 clean SEO-indexability cycles (c59/c63/c64/c65), pivot to a GENUINELY
// uncovered surface: image alt-text coverage. Dual value — image-SEO (a wallcoverings
// catalog where imagery IS the product → Google Images traffic) AND ADA/WCAG
// accessibility (real legal-exposure surface). Source of truth = product.json
// images[].alt (what Steve controls + what the theme renders). Per product/image we
// measure: has >=1 image; image alt populated vs empty/missing; alt "meaningful"
// (contains the SKU or a title token, not just brand boilerplate); duplicate alt
// across a product's images (image-SEO weakness, not an a11y failure).
// Recon: alt is auto-constructed (SKU + pattern name + brand) → expect decent coverage.
// NEGATIVE CONTROL: print actual alt VALUES — confirm the parser flags a genuinely
// empty/null alt as empty AND a populated alt as populated (not blind either way).
// EXCLUDES Phillip Jeffries.
const WWW='https://www.designerwallcoverings.com';
const UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function get(u){for(let a=0;a<4;a++){try{const r=await fetch(u,{headers:{'User-Agent':UA}});if(r.status===429){await sleep(1500*(a+1));continue;}return {status:r.status,text:await r.text()};}catch(e){await sleep(800*(a+1));}}return {status:0,text:''};}
const PJ=/phillip[- ]?jeffries|phillip-jeffries/i;
const norm=s=>(s||'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim();

// stratified handle pool across all non-locale product sitemaps
const idx=await get(`${WWW}/sitemap.xml`);
const prodMaps=[...idx.text.matchAll(/<loc>([^<]+sitemap_products_\d+\.xml[^<]*)<\/loc>/g)].map(m=>m[1].replace(/&amp;/g,'&')).filter(u=>!/\/en-(ca|gb)\//.test(u));
let handles=[];
for(const sm of prodMaps){
  const x=await get(sm);
  const hs=[...x.text.matchAll(/<loc>[^<]*\/products\/([^<\/?]+)/g)].map(m=>m[1]);
  const step=Math.max(1,Math.floor(hs.length/8));
  for(let i=0;i<hs.length && handles.length<360;i+=step) handles.push(hs[i]);
}
handles=[...new Set(handles)].filter(h=>!PJ.test(h)).slice(0,300);
console.log(`=== image-alt-audit (READ-ONLY, $0) ===\nsampling ${handles.length} live products across ${prodMaps.length} sitemaps (PJ excluded)\n`);

let prodEval=0, prodNoImage=0, prodAllAlt=0, prodSomeMissing=0, prodAllMissing=0;
let imgTotal=0, imgAlt=0, imgEmpty=0, imgMeaningful=0, imgDupAlt=0;
let pjSkip=0, fetchErr=0;
const emptySamples=[], populatedSamples=[], problems=[];
let done=0;
for(const h of handles){
  const r=await get(`${WWW}/products/${h}.json`);
  let prod; try{ prod=JSON.parse(r.text).product; }catch{ fetchErr++; done++; continue; }
  if(!prod){ fetchErr++; done++; continue; }
  if(PJ.test(prod.vendor||'')||PJ.test(h)){ pjSkip++; done++; continue; }
  const imgs=prod.images||[];
  prodEval++;
  if(imgs.length===0){ prodNoImage++; problems.push({h,issue:'NO-IMAGE'}); done++; await sleep(70); continue; }
  const sku=norm(prod.variants&&prod.variants[0]&&prod.variants[0].sku);
  const titleToks=norm(prod.title).split(' ').filter(t=>t.length>3);
  let missing=0, present=0; const altsSeen=new Set();
  for(const im of imgs){
    imgTotal++;
    const alt=(im.alt==null?'':String(im.alt)).trim();
    if(alt.length===0){ imgEmpty++; missing++; if(emptySamples.length<8) emptySamples.push({h,id:im.id,raw:JSON.stringify(im.alt)}); }
    else {
      imgAlt++; present++;
      if(populatedSamples.length<5) populatedSamples.push({h,alt:alt.slice(0,70)});
      const na=norm(alt);
      const meaningful = (sku && na.includes(sku)) || titleToks.some(t=>na.includes(t));
      if(meaningful) imgMeaningful++;
      if(altsSeen.has(na)) imgDupAlt++; else altsSeen.add(na);
    }
  }
  if(missing===0) prodAllAlt++;
  else if(present===0){ prodAllMissing++; problems.push({h,issue:`ALL-${imgs.length}-IMG-NO-ALT`}); }
  else { prodSomeMissing++; problems.push({h,issue:`${missing}/${imgs.length}-IMG-NO-ALT`}); }
  done++;
  if(done%50===0) console.log(`  ${done}/${handles.length} (img alt ${imgAlt}/${imgTotal}; no-image prods ${prodNoImage})`);
  await sleep(70);
}

console.log(`\n=== RESULTS ===`);
console.log(`products evaluated: ${prodEval} | PJ-skipped: ${pjSkip} | fetch/parse-err: ${fetchErr}`);
console.log(`\n-- product-level --`);
console.log(`  products with 0 images: ${prodNoImage}`);
console.log(`  all images have alt (healthy): ${prodAllAlt}`);
console.log(`  some images missing alt: ${prodSomeMissing}`);
console.log(`  ALL images missing alt (a11y fail): ${prodAllMissing}`);
console.log(`\n-- image-level (${imgTotal} images) --`);
console.log(`  alt populated: ${imgAlt}  (${imgTotal?(100*imgAlt/imgTotal).toFixed(1):0}%)`);
console.log(`  alt EMPTY/missing: ${imgEmpty}  (${imgTotal?(100*imgEmpty/imgTotal).toFixed(1):0}%)`);
console.log(`  populated alt that is MEANINGFUL (SKU/title token): ${imgMeaningful}/${imgAlt}  (${imgAlt?(100*imgMeaningful/imgAlt).toFixed(1):0}% of populated)`);
console.log(`  duplicate alt within same product (image-SEO weakness): ${imgDupAlt}`);
if(imgTotal>0){
  const p=imgEmpty/imgTotal, se=Math.sqrt(p*(1-p)/imgTotal), lo=Math.max(0,p-1.96*se)*100, hi=Math.min(1,p+1.96*se)*100;
  console.log(`\n*** image alt-MISSING rate: ${imgEmpty}/${imgTotal} = ${(100*p).toFixed(1)}%  (95% CI ${lo.toFixed(1)}–${hi.toFixed(1)}%, n=${imgTotal}) ***`);
}
console.log(`\n-- NEGATIVE CONTROL (parser reads alt values, not blind) --`);
console.log(`  populated-alt samples (detected as populated):`); populatedSamples.forEach(s=>console.log(`    ${s.h.slice(0,34)} → "${s.alt}"`));
if(emptySamples.length){ console.log(`  empty-alt samples (raw value, detected as empty):`); emptySamples.forEach(s=>console.log(`    ${s.h.slice(0,34)} img=${s.id} raw=${s.raw}`)); }
else console.log(`  (no empty-alt images in sample → cannot show an empty detection; populated detection is proven above)`);
if(problems.length){ console.log(`\nproblems (first 15):`); problems.slice(0,15).forEach(x=>console.log(`  ${x.issue}: ${x.h.slice(0,48)}`)); }

import fs from 'fs';
fs.writeFileSync('/tmp/image-alt-audit.json',JSON.stringify({ts:new Date().toISOString(),sampled:handles.length,prodEval,prodNoImage,prodAllAlt,prodSomeMissing,prodAllMissing,imgTotal,imgAlt,imgEmpty,imgMeaningful,imgDupAlt,pjSkip,fetchErr,emptySamples,populatedSamples,problems},null,2));
console.log('\nwrote /tmp/image-alt-audit.json');