← back to Hollywood Import
hollywood-remap-enumerate.mjs
96 lines
// TK-10679 Phase 2 — READ-ONLY dry-run enumerate + restore-map builder.
// Reuses the canary's OWN definitions (FABRICATED / deriveBase / planProduct) so the
// remediation set is exactly the set the canary measures. Writes NOTHING to Shopify.
//
// Output (all local, $0):
// remap-dryrun.json — per-product verdict + change counts (summary)
// remap-restore-map.json — full {product_gid, handle, field, old, new} for every write
// (this IS the undo: to revert, write `old` back)
// remap-held.csv — affected products with NO recoverable real code (never fabricate)
import { gql } from '/Users/macstudio3/.claude/skills/dw-hollywood-sku-canary/shopify.mjs';
import { FABRICATED, deriveBase, planProduct } from '/Users/macstudio3/.claude/skills/dw-hollywood-sku-canary/lib.mjs';
import { writeFileSync } from 'node:fs';
const VENDOR = 'Hollywood Wallcoverings';
const PAGE = `query($cursor:String){
products(first:50, query:"vendor:\\"${VENDOR}\\"", after:$cursor){
pageInfo{ hasNextPage endCursor }
nodes{
id handle title status vendor
mfG: metafield(namespace:"global", key:"dw_sku"){ value }
mfD: metafield(namespace:"dwc", key:"dw_sku"){ value }
mfC: metafield(namespace:"custom", key:"dw_sku"){ value }
pn: metafield(namespace:"dwc", key:"pattern_number"){ value }
variants(first:20){ nodes{ id title sku selectedOptions{ name value } inventoryItem{ id } } }
}
}
}`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const isFab = s => !!s && FABRICATED.test(s);
const baseOf = sku => (sku || '').replace(/-(sample|yard|roll)$/i, '');
// A product is AFFECTED iff any of its 3 dw_sku metafields OR any variant base is fabricated.
function affected(p) {
if (isFab(p.mfG?.value) || isFab(p.mfD?.value) || isFab(p.mfC?.value)) return true;
return p.variants.nodes.some(v => isFab(baseOf(v.sku)));
}
const summary = { scanned: 0, affected: 0, remappable: 0, held: 0, conflict: 0,
variant_writes: 0, metafield_writes: 0, held_products: [], conflict_products: [] };
const restoreMap = []; // {product_gid, handle, field, old, new}
const heldRows = []; // affected but no recoverable code
let cursor = null;
while (true) {
let res;
for (let a = 0; ; a++) {
try { res = await gql(PAGE, { cursor }); break; }
catch (e) { if (/THROTTLED|throttle/i.test(e.message) && a < 8) { await sleep(2000 * (a + 1)); continue; } throw e; }
}
for (const p of res.data.products.nodes) {
summary.scanned++;
if (!affected(p)) continue;
summary.affected++;
const base = deriveBase(p);
if (base === null) {
summary.held++;
summary.held_products.push(p.handle);
const cur = p.mfG?.value || p.mfD?.value || p.mfC?.value || p.variants.nodes.map(v => v.sku).find(isFab) || '';
heldRows.push(`${p.handle},${p.title.replace(/,/g, ' ')},${cur}`);
continue;
}
if (typeof base === 'object' && base.conflict) {
summary.conflict++;
summary.conflict_products.push({ handle: p.handle, candidates: base.conflict });
continue;
}
// remappable — build the plan (from/to for each variant SKU + each dw_sku metafield)
summary.remappable++;
const { varChanges, mfChanges } = planProduct(p, base);
for (const c of varChanges) {
summary.variant_writes++;
restoreMap.push({ product_gid: p.id, handle: p.handle, field: `variant:${c.variantId}`,
inv_item: c.invItemId, old: c.from, new: c.to });
}
for (const c of mfChanges) {
summary.metafield_writes++;
restoreMap.push({ product_gid: p.id, handle: p.handle, field: `metafield:${c.namespace}.dw_sku`,
old: c.from, new: c.to });
}
}
if (!res.data.products.pageInfo.hasNextPage) break;
cursor = res.data.products.pageInfo.endCursor;
await sleep(300);
}
writeFileSync(new URL('./remap-dryrun.json', import.meta.url).pathname, JSON.stringify(summary, null, 2));
writeFileSync(new URL('./remap-restore-map.json', import.meta.url).pathname, JSON.stringify(restoreMap, null, 1));
writeFileSync(new URL('./remap-held.csv', import.meta.url).pathname, 'handle,title,current_fabricated_code\n' + heldRows.join('\n') + '\n');
console.log(JSON.stringify({
scanned: summary.scanned, affected: summary.affected,
remappable: summary.remappable, held: summary.held, conflict: summary.conflict,
total_writes: summary.variant_writes + summary.metafield_writes,
variant_writes: summary.variant_writes, metafield_writes: summary.metafield_writes,
}, null, 2));