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