← 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();