← back to Dw Kravet Hires

scripts/build-rescrape-lane.mjs

99 lines

#!/usr/bin/env node
// TK-12097 — characterize the 763 A/C/D no-source deferred products into a RE-SCRAPE LANE.
// Buckets each product by WHY it had no hi-res + the recommended recovery method + priority.
// For the "hero-render-404" bucket it RE-MEASURES the original .auto Brandfolder hero (which the
// phase-2 recover wrongly rewrote to .jpg) to CONFIRM it is immediately recoverable. $0 read-only.
import fs from 'fs';
import path from 'path';
import https from 'https';

const D = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../data/tk12097');
const g = (f) => JSON.parse(fs.readFileSync(path.join(D, f), 'utf8'));

// measure max-dim of an image via a ranged GET (JPEG/PNG header parse, no deps)
function measure(url) {
  return new Promise((resolve) => {
    const req = https.get(url, { headers: { 'User-Agent': 'dw-rescrape TK-12097', Range: 'bytes=0-131071' }, timeout: 25000 }, (res) => {
      if (res.statusCode !== 200 && res.statusCode !== 206) { res.resume(); return resolve({ ok: false, http: res.statusCode }); }
      const chunks = [];
      res.on('data', (c) => chunks.push(c));
      res.on('end', () => {
        const b = Buffer.concat(chunks);
        let w = 0, h = 0;
        if (b.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) { w = b.readUInt32BE(16); h = b.readUInt32BE(20); }
        else if (b[0] === 0xff && b[1] === 0xd8) { let i = 2; while (i < b.length - 9) { if (b[i] !== 0xff) { i++; continue; } const m = b[i + 1]; if (m >= 0xc0 && m <= 0xc3) { h = b.readUInt16BE(i + 5); w = b.readUInt16BE(i + 7); break; } i += 2 + b.readUInt16BE(i + 2); } }
        resolve({ ok: w > 0, http: res.statusCode, le: Math.max(w, h) });
      });
    });
    req.on('error', () => resolve({ ok: false, http: 0 }));
    req.on('timeout', () => { req.destroy(); resolve({ ok: false, http: 0 }); });
  });
}

const A = g('batchA-phase2-results.json');
const Dd = g('batchD-phase2-results.json');
const C = (g('batchC-no-hires-source.json').rows) || g('batchC-no-hires-source.json');

function classifyPhase2(rows, batch, vendorDefault) {
  const out = [];
  for (const r of rows.filter((x) => x.status === 'absent')) {
    const reason = r.reason || '';
    let bucket, method, priority;
    if (/hero render/.test(reason)) { bucket = 'recoverable_now_auto_url'; method = 'use the .auto Brandfolder hero URL as-is (phase-2 wrongly forced .jpg on /as/ assets)'; priority = 1; }
    else if (/no vendor page/.test(reason)) { bucket = 'reresolve_slug'; method = 'kravet.com naive-slug 404 → re-resolve via stored product_url / pattern-name slug / site search'; priority = 2; }
    else if (/no upgrade over/.test(reason)) { bucket = 'vendor_capped_lowres'; method = 'vendor hero is itself ≤400px — no hi-res exists at vendor; recheck quarterly, do not re-scrape now'; priority = 4; }
    else if (/placeholder/.test(reason)) { bucket = 'placeholder_or_discontinued'; method = 'vendor serves placeholder / discontinued — accept low-res or archive-review'; priority = 5; }
    else { bucket = 'other_absent'; method = reason; priority = 3; }
    out.push({ batch, shopify_id: r.shopify_id, vendor: r.vendor || vendorDefault, mfr_sku: r.mfr_sku,
      dw_sku: r.dw_sku, cur_le: r.cur_le, discontinued: !!r.discontinued, bucket, method, priority,
      hero_asset: r.hero_asset || null });
  }
  return out;
}

const items = [
  ...classifyPhase2(A, 'A', 'Kravet-family'),
  ...classifyPhase2(Dd, 'D', 'Gaston Y Daniela'),
  ...C.map((r) => ({ batch: 'C', shopify_id: r.shopify_id, vendor: 'Hollywood Wallcoverings', mfr_sku: r.mfr_sku,
    dw_sku: r.dw_sku, cur_le: r.cur_le, discontinued: false,
    bucket: 'no_vendor_mapping', priority: 3,
    method: 'no momentum_colorways code match → re-scrape momentum live catalog / fix Hollywood↔momentum code mapping',
    hero_asset: null })),
];

const main = async () => {
  // CONFIRM the recoverable_now bucket by re-measuring the .auto hero (sample up to 30)
  const rec = items.filter((i) => i.bucket === 'recoverable_now_auto_url' && i.hero_asset);
  const sample = rec.slice(0, 30);
  let confirmed = 0;
  for (const r of sample) { const m = await measure(r.hero_asset); r.auto_measured_le = m.le || null; r.auto_http = m.http; if (m.ok && m.le >= 800) confirmed++; }
  const confirmRate = sample.length ? (confirmed / sample.length) : 0;

  // rollups
  const byBucket = {}, byVendorBucket = {};
  for (const i of items) {
    byBucket[i.bucket] = (byBucket[i.bucket] || 0) + 1;
    const k = `${i.vendor} | ${i.bucket}`;
    byVendorBucket[k] = (byVendorBucket[k] || 0) + 1;
  }
  const manifest = {
    ticket: 'TK-12097', lane: 'no-source re-scrape', generated_at: new Date().toISOString(),
    generated_by: 'vp-dw-commerce', total: items.length,
    scope: 'The 763 A/C/D no-source deferred products (Batch B excluded — 848 need a separate JS external re-scrape).',
    buckets: {
      recoverable_now_auto_url: { priority: 1, note: 'immediate — .auto hero valid, phase-2 .jpg-forcing bug; fold into a Batch D-plus', count: byBucket.recoverable_now_auto_url || 0, confirm_sample: sample.length, confirmed_ge_800: confirmed, confirm_rate: +confirmRate.toFixed(2) },
      no_vendor_mapping: { priority: 3, note: 'Hollywood — needs momentum live re-scrape / code-map fix', count: byBucket.no_vendor_mapping || 0 },
      reresolve_slug: { priority: 2, note: 'kravet.com slug miss — re-resolve via product_url/search', count: byBucket.reresolve_slug || 0 },
      vendor_capped_lowres: { priority: 4, note: 'vendor genuinely has only ≤400px — recheck quarterly, no re-scrape now', count: byBucket.vendor_capped_lowres || 0 },
      placeholder_or_discontinued: { priority: 5, note: 'placeholder/discontinued — accept or archive-review', count: byBucket.placeholder_or_discontinued || 0 },
      other_absent: { priority: 3, note: 'misc', count: byBucket.other_absent || 0 },
    },
    by_vendor_bucket: byVendorBucket,
    discontinued_count: items.filter((i) => i.discontinued).length,
    items,
  };
  fs.writeFileSync(path.join(D, 'rescrape-lane-manifest.json'), JSON.stringify(manifest, null, 1));
  console.log(JSON.stringify({ total: manifest.total, buckets: manifest.buckets, discontinued: manifest.discontinued_count }, null, 2));
};
main();