← back to Dw Kravet Hires
scripts/build-oversized-subset.mjs
77 lines
#!/usr/bin/env node
// TK-11740 — build the OVERSIZED-RESIDUAL subset swap-map.
//
// The 112 residual variants (data/hires-oversized-residual.txt) self-healed under the
// parent TK-11658 run because their Brandfolder hi-res originals exceed Shopify's ~20MP
// (>~5000px) media processing limit. Brandfolder's CDN supports on-the-fly resize via
// ?width=W&height=H&fit=bound (aspect-preserving, verified). This builds a subset map
// whose proposed_hires_url is the SAME source with the resize param appended, so the
// existing apply-hires.mjs executor swaps a <=4472px (<=20MP) hi-res in place of the 400px.
//
// Output: data/kravet-hires-oversized-subset-map.json (same envelope apply-hires reads)
// $0, local, no network, no Shopify write.
import fs from 'fs';
import path from 'path';
const HERE = path.dirname(new URL(import.meta.url).pathname);
const PROJ = path.resolve(HERE, '..');
const MAP = path.join(PROJ, 'data/kravet-hires-swap-map.json');
const RESID = path.join(PROJ, 'data/hires-oversized-residual.txt');
const BROKEN = path.join(PROJ, 'data/hires-broken-source.txt');
const OUT = path.join(PROJ, 'data/kravet-hires-oversized-subset-map.json');
// Shopify media limits are BOTH <=20MP (dims) AND <=20MB (file bytes). fit=bound caps dims but
// the delivered file must also stay <20MB. Two source classes need different params:
// JPEG: &quality=90 at 4472px → ~6MB (lossy compresses well; full 20MP retained).
// PNG : Brandfolder IGNORES &quality AND &format on PNG (stays lossless ~1.6 B/px), so a 4472px
// PNG is ~20-24MB → fails. Cap DIMENSIONS to 3000px instead → ~11-15MB, aspect preserved.
// (TK-11740, 2026-09-14: first run failed 53 on the 20MB byte limit; PNGs need the dim cap.)
const RESIZE_JPEG = '?width=4472&height=4472&fit=bound&quality=90';
const RESIZE_PNG = '?width=3000&height=3000&fit=bound';
const resizeFor = (src) => (/\.png(\?|$)/i.test(src) ? RESIZE_PNG : RESIZE_JPEG);
const doc = JSON.parse(fs.readFileSync(MAP, 'utf8'));
const byMfr = new Map(doc.rows.map((r) => [r.mfr_sku, r]));
const resid = fs.readFileSync(RESID, 'utf8').split('\n').map((s) => s.trim()).filter(Boolean);
// EXCLUDE broken-source variants (dead 404 / unprocessable 422 PNGs) — resizing cannot
// fix an unfetchable source; those need a fresh vendor re-scrape (out of scope, split off).
const broken = new Set(fs.existsSync(BROKEN)
? fs.readFileSync(BROKEN, 'utf8').split('\n').map((s) => s.split('\t')[0].trim()).filter(Boolean)
: []);
const missing = resid.filter((k) => !byMfr.has(k));
if (missing.length) { console.error('FATAL: residual keys not in map:', missing); process.exit(2); }
const fixable = resid.filter((k) => !broken.has(k));
const rows = fixable.map((k) => {
const r = byMfr.get(k);
if (!r.swappable_from_local_staging) throw new Error(`not swappable: ${k}`);
const src = r.proposed_hires_url;
if (src.includes('?')) throw new Error(`source already has query string: ${k} ${src}`);
const rp = resizeFor(src);
return { ...r, proposed_hires_url: src + rp,
original_oversized_url: src, resize_param: rp, subset_ticket: 'TK-11740' };
});
const out = {
ticket: 'TK-11740',
parent_ticket: 'TK-11658',
generated_at: new Date().toISOString(),
generated_by: process.env.TK_AGENT || 'night-TK-11740',
store: doc.store,
scope: `The oversized-source residual variants from TK-11658 whose Brandfolder source is FETCHABLE; proposed_hires_url resized to <=4472px (<=20MP) via Brandfolder CDN ?width=4472&height=4472&fit=bound. Excludes ${broken.size} broken-source variants (see data/hires-broken-source.txt) that need a re-scrape.`,
resize_param: { jpeg: RESIZE_JPEG, png: RESIZE_PNG },
residual_total: resid.length,
excluded_broken_source: broken.size,
rollback: doc.rollback,
rows,
};
fs.writeFileSync(OUT, JSON.stringify(out, null, 1));
console.log(`wrote ${OUT}`);
console.log(`residual total: ${resid.length} excluded(broken-source): ${broken.size}`);
console.log(`rows: ${rows.length}`);
console.log(`sample: ${rows[0].mfr_sku}`);
console.log(` old (oversized): ${rows[0].original_oversized_url}`);
console.log(` new (resized) : ${rows[0].proposed_hires_url}`);