← back to Dw Unbuyable Recovery Pilot
tk10978-rebelwalls/rescrape-recon.mjs
77 lines
#!/usr/bin/env node
// TK-10978 — Rebel Walls RS-494 corrected re-scrape RECON (READ-ONLY, $0, feed-first).
// Fetches the vendor's TRUE per-m2 price from JSON-LD, keyed RS<n> -> vendor R<n>.
// SAFETY NET: only accept a price when JSON-LD sku DIGITS == our RS digits (rejects wrong-slug hits).
// Writes ONLY local artifacts (data/rescrape-recon.csv + data/rescrape-summary.json).
// NO writes to Shopify or dw_unified. Gentle: 1 request / 1.5s, single-threaded (skill's stated rate).
import { writeFileSync, mkdirSync } from 'node:fs';
import { execSync } from 'node:child_process';
const HERE = new URL('.', import.meta.url).pathname;
mkdirSync(`${HERE}data`, { recursive: true });
const SQL = `BEGIN READ ONLY;
SELECT json_agg(t) FROM (
SELECT regexp_replace(shopify_id,'.*/','') AS pid, title, variant_sku AS sample_sku, mfr_sku
FROM shopify_products
WHERE vendor='Rebel Walls' AND status='ACTIVE'
AND NOT coalesce(has_product_variant,false) AND variant_sku ILIKE '%-Sample'
) t;
ROLLBACK;`;
const out = execSync('psql -h /tmp -d dw_unified -tA -v ON_ERROR_STOP=1', { input: SQL, encoding: 'utf8', maxBuffer: 64*1024*1024 });
const rows = JSON.parse(out.slice(out.indexOf('['), out.lastIndexOf(']')+1) || '[]');
const slugify = t => t.replace(/ \| Rebel Walls\s*$/i,'').toLowerCase()
.replace(/['’]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
const digits = s => (String(s||'').match(/\d+/)||[''])[0];
const sh = s => `"${String(s??'').replace(/"/g,'""')}"`;
const results = [];
for (let i=0;i<rows.length;i++){
const r = rows[i];
const slug = slugify(r.title);
const rsDig = digits(r.mfr_sku);
let http='ERR', ldsku='', price='', avail='', cls='needs_scraper';
try {
const html = execSync(`curl -s -m 25 -w '\\n<<HTTP:%{http_code}>>' -A 'Mozilla/5.0 (compatible; DW-catalog/1.0)' "https://rebelwalls.com/${slug}"`, {encoding:'utf8', maxBuffer:16*1024*1024});
http = (html.match(/<<HTTP:(\d+)>>/)||[])[1] || '000';
if (http==='200'){
const m = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
if (m){
try {
const d = JSON.parse(m[1]);
const it = (Array.isArray(d)?d:[d]).find(x=>x&&x['@type']==='Product');
if (it){ ldsku=it.sku||''; const off=it.offers||{}; price=off.price??''; avail=String(off.availability||'').split('/').pop(); }
} catch {}
}
if (!ldsku) cls='slug_unresolved';
else if (digits(ldsku)!==rsDig) cls='sku_mismatch'; // wrong product -> reject
else if (avail && avail!=='InStock') cls='oos_defer';
else if (Number(price)>10 && Number(price)<10000) cls='recoverable';
else cls='no_price';
} else if (http==='410') cls='discontinued_410';
else if (http==='404') cls='slug_404';
else cls='http_'+http;
} catch(e){ cls='fetch_error'; }
results.push({ pid:r.pid, title:r.title, rs_sku:r.mfr_sku, r_map:'R'+rsDig, slug, http, ldsku, per_sqm:price, avail, cls });
if ((i+1)%25===0) process.stderr.write(` ...${i+1}/${rows.length}\n`);
execSync('sleep 1.5');
}
const cols=['pid','title','rs_sku','r_map','slug','http','ldsku','per_sqm','avail','cls'];
writeFileSync(`${HERE}data/rescrape-recon.csv`, [cols.join(',')].concat(results.map(x=>cols.map(c=>sh(x[c])).join(','))).join('\n'));
const by = results.reduce((a,x)=>{a[x.cls]=(a[x.cls]||0)+1;return a;},{});
const recov = results.filter(x=>x.cls==='recoverable');
const prices = recov.map(x=>Number(x.per_sqm));
const summary = {
ticket:'TK-10978', mode:'READ-ONLY recon — NO writes', generated_at:new Date().toISOString(),
total:results.length, by_class:by,
recoverable:recov.length,
per_sqm_min: prices.length?Math.min(...prices):null,
per_sqm_max: prices.length?Math.max(...prices):null,
per_sqm_distinct: [...new Set(prices)].sort((a,b)=>a-b),
defer_discontinued_or_unresolved: results.length - recov.length,
note:'recoverable = live 200 + JSON-LD sku digits == RS digits + InStock + price in range. Everything else defers to the scraper or is vendor-discontinued.',
};
writeFileSync(`${HERE}data/rescrape-summary.json`, JSON.stringify(summary,null,2));
console.log(JSON.stringify(summary,null,2));