← back to Dw Kravet Hires

scripts/build-batchA-final.mjs

122 lines

#!/usr/bin/env node
// TK-12097 Batch A — merge the two hi-res sources into the final apply-hires swap map.
//   source 1: data/tk12097/batchA-swap-map.json  (vendor _catalog Brandfolder, measured final_le)
//   source 2: data/tk12097/batchA-phase2-results.json (kravet.com Brandfolder hero, measured long_edge)
// For each of the 814 low-res products pick the LARGER genuine upgrade (>400px). Records hires_le
// per row so the floor is a knob at approval time. Emits the final map + a width histogram.
//
// FLOOR: env MIN_UPGRADE (default 401 = every genuine upgrade). The dry-run/summary shows per-floor
// coverage so Steve sets the customer-facing quality floor at the GATED live-apply approval.
import fs from 'fs';
import path from 'path';

const HERE = path.dirname(new URL(import.meta.url).pathname);
const PROJ = path.resolve(HERE, '..');
const D = path.join(PROJ, 'data/tk12097');
const FLOOR = parseInt(process.env.MIN_UPGRADE || '401', 10);

const lowres = JSON.parse(fs.readFileSync(path.join(D, 'batchA-lowres-all.json'), 'utf8')); // 814 authoritative low-res
const catMap = JSON.parse(fs.readFileSync(path.join(D, 'batchA-swap-map.json'), 'utf8'));   // _catalog ≥800 swappable
const p2 = JSON.parse(fs.readFileSync(path.join(D, 'batchA-phase2-results.json'), 'utf8')); // kravet.com heroes

// index sources by shopify_id
const catBy = new Map();
for (const r of catMap.rows) catBy.set(r.shopify_id, { url: r.proposed_hires_url, le: r.hires_le, src: 'vendor_catalog_brandfolder', capped: r.preflight_capped });
const p2By = new Map();
for (const r of p2) {
  if ((r.status === 'recovered' || r.status === 'below_threshold') && r.recovered_hires_url && r.long_edge > 400) {
    p2By.set(r.shopify_id, { url: r.recovered_hires_url, le: r.long_edge, src: 'kravet_com_brandfolder_hero', capped: false });
  }
}

// COLORWAY SAFETY: a phase-2 kravet.com hero is trusted only when its filename contains the
// full product SKU code (proven-same-colorway). _catalog rows are colorway-exact by construction.
// Rows where only the COLOR suffix matches but the pattern base differs (legacy cross-ref codes)
// are PLAUSIBLE but unproven -> held in tier2 for verification, NEVER auto-swapped.
const codeOf = (mfr) => String(mfr).replace(/\.0$/, '').replace(/^[PT]/i, '').replace(/[^A-Za-z0-9]/g, '').toUpperCase();
const fnameAlnum = (url) => url.split('?')[0].split('/').pop().replace(/[^A-Za-z0-9]/g, '').toUpperCase();
const colorwaySafe = (r, mfr) => r.src === 'vendor_catalog_brandfolder' || fnameAlnum(r.url).includes(codeOf(mfr));

const rows = [];
const tier2 = [];
const noSource = [];
for (const lr of lowres) {
  const cands = [];
  if (catBy.has(lr.shopify_id)) cands.push(catBy.get(lr.shopify_id));
  if (p2By.has(lr.shopify_id)) cands.push(p2By.get(lr.shopify_id));
  const genuine = cands.filter((c) => c.le > (lr.cur_le || 400));
  if (!genuine.length) { noSource.push(lr); continue; }
  // prefer a colorway-SAFE candidate; only fall to unsafe if no safe upgrade exists
  const safe = genuine.filter((c) => colorwaySafe(c, lr.mfr_sku));
  if (!safe.length) {
    const best2 = genuine.sort((a, b) => b.le - a.le)[0];
    tier2.push({ shopify_id: lr.shopify_id, vendor: lr.vendor, mfr_sku: lr.mfr_sku, dw_sku: lr.dw_sku,
      cur_width: lr.cur_le, rollback_url: lr.rollback_url, proposed_hires_url: best2.url, hires_le: best2.le,
      hires_source: best2.src, reason: 'colorway_unverified_filename_pattern_mismatch' });
    continue;
  }
  const best = safe.sort((a, b) => b.le - a.le)[0];
  rows.push({
    shopify_id: lr.shopify_id,
    vendor: lr.vendor,
    dw_sku: lr.dw_sku,
    mfr_sku: lr.mfr_sku,
    cur_width: lr.cur_le,
    current_400px_url: lr.rollback_url,
    rollback_url: lr.rollback_url,
    proposed_hires_url: best.url,
    hires_le: best.le,
    hires_source: best.src,
    preflight_capped: best.capped,
    swappable_from_local_staging: true,
  });
}

// histogram + per-floor coverage
const bands = [[401, 500], [500, 800], [800, 1200], [1200, 2500], [2500, 99999]];
const hist = {};
for (const [lo, hi] of bands) hist[`${lo}-${hi === 99999 ? '+' : hi}`] = rows.filter((r) => r.hires_le >= lo && r.hires_le < hi).length;
const floors = [500, 600, 800, 1000, 1200];
const perFloor = {};
for (const f of floors) perFloor[`>=${f}`] = rows.filter((r) => r.hires_le >= f).length;
const byVendor = {};
for (const r of rows) byVendor[r.vendor] = (byVendor[r.vendor] || 0) + 1;
const bySource = {};
for (const r of rows) bySource[r.hires_source] = (bySource[r.hires_source] || 0) + 1;

// apply the FLOOR to the emitted map
const applied = rows.filter((r) => r.hires_le >= FLOOR);

const doc = {
  ticket: 'TK-12097', batch: 'A',
  parent: 'TK-12090 / TK-11494',
  generated_at: new Date().toISOString(),
  generated_by: 'vp-dw-commerce',
  store: 'designer-laboratory-sandbox',
  engine: 'reuse ~/Projects/dw-kravet-hires/scripts/apply-hires.mjs (--max-width 400)',
  floor_applied: FLOOR,
  scope: `Batch A Kravet-family (Donghia, B&F, Lee Jofa, GP&J Baker). 814 live-measured low-res(<=400px) ACTIVE products. `
    + `Hi-res resolved from vendor _catalog Brandfolder (colorway-exact) and kravet.com Brandfolder hero (phase-2), `
    + `LARGER genuine upgrade chosen per product, MEASURED. Old low-res media retained per product (rollback anchor).`,
  counts: {
    lowres_total: lowres.length,
    colorway_safe_upgrades: rows.length,
    tier2_held_colorway_unverified: tier2.length,
    no_hires_source: noSource.length,
    emitted_at_floor: applied.length,
    by_vendor_safe_upgrades: byVendor,
    by_source_safe_upgrades: bySource,
    width_histogram_safe_upgrades: hist,
    coverage_per_floor_safe: perFloor,
  },
  rows: applied,
};
fs.writeFileSync(path.join(D, 'batchA-final-map.json'), JSON.stringify(doc, null, 1));
// also persist the full safe superset + tier2 + no-source for the follow-on / verification
fs.writeFileSync(path.join(D, 'batchA-all-safe-upgrades.json'), JSON.stringify({ rows }, null, 1));
fs.writeFileSync(path.join(D, 'batchA-tier2-colorway-unverified.json'), JSON.stringify({ rows: tier2 }, null, 1));
fs.writeFileSync(path.join(D, 'batchA-no-hires-source.json'), JSON.stringify(noSource, null, 1));

console.log(JSON.stringify(doc.counts, null, 2));
console.log(`\nfloor applied = ${FLOOR}px -> ${applied.length} rows written to batchA-final-map.json`);