← back to Tk10630 Sku Suffix Canary

leak-fix-plan.mjs

57 lines

// Build the Chesapeake/Brewster/Malibu-family leak fix DRAFT (no writes). Per real
// leaking term, gather affected active products and compute proposed fixes:
//   handle -> strip the token (GATED: needs 301 = blocked on TK-10640)
//   tags   -> drop tags containing the token (unblocked)
//   alt    -> replace token with the public brand (maps_to) (unblocked)
//   title  -> replace token with the public brand (unblocked)
//   nicolette (archived brand) -> ARCHIVE the product (unblocked, gated)
import { readFileSync, writeFileSync } from 'node:fs';
import { gql } from './shopify.mjs';

const denylist = JSON.parse(readFileSync(`${process.env.HOME}/.claude/skills/dw-leak-scanner/denylist.json`, 'utf8'));
const mapsTo = Object.fromEntries(denylist.terms.map(t => [t.term.toLowerCase().split(/\s+/)[0], t.maps_to]));
const TERMS = ['chesapeake', 'brewster', 'carlsten', 'lillian', 'greenland', 'momentum']; // nicolette handled as ARCHIVE
const publicBrand = term => {
  const m = (mapsTo[term] || '').split('(')[0].trim();
  return m || 'Designer Wallcoverings';
};
const tok = term => new RegExp(`(^|[^a-z0-9])${term}([^a-z0-9]|$)`, 'i');
const tokG = term => new RegExp(`[-_ ]?${term}[- _]?`, 'ig');

const plan = { handle: [], tag: [], alt: [], title: [], archive: [] };
async function gather(term) {
  const re = tok(term), seen = new Map();
  for (const field of ['handle', 'title', 'tag']) {
    let cursor = null, guard = 0;
    while (guard++ < 20) {
      const { data } = await gql(`query($c:String){ products(first:100, query:"${field}:*${term}* status:ACTIVE", after:$c){ pageInfo{hasNextPage endCursor} nodes{ id handle title tags images(first:10){ nodes{ id altText } } } } }`, { c: cursor });
      for (const p of data.products.nodes) if (!seen.has(p.id)) seen.set(p.id, p);
      if (!data.products.pageInfo.hasNextPage) break;
      cursor = data.products.pageInfo.endCursor;
    }
  }
  return { re, seen };
}

for (const term of TERMS) {
  const brand = publicBrand(term);
  const { re, seen } = await gather(term);
  for (const p of seen.values()) {
    if (re.test(p.handle)) { const nh = p.handle.replace(tokG(term), '-').replace(/^-|-$/g, '').replace(/--+/g, '-'); if (nh !== p.handle) plan.handle.push({ id: p.id, term, from: p.handle, to: nh }); }
    if (re.test(p.title)) plan.title.push({ id: p.id, term, from: p.title, to: p.title.replace(new RegExp(term, 'ig'), brand) });
    const badTags = (p.tags || []).filter(x => re.test(x)); if (badTags.length) plan.tag.push({ id: p.id, term, remove: badTags });
    for (const im of p.images.nodes) if (re.test(im.altText || '')) plan.alt.push({ id: p.id, mediaId: im.id, term, from: im.altText, to: (im.altText || '').replace(new RegExp(term, 'ig'), brand) });
  }
}
// nicolette (archived brand) -> archive
{ const { seen } = await gather('nicolette'); for (const p of seen.values()) plan.archive.push({ id: p.id, handle: p.handle, reason: 'Nicolette Mayer is an archived brand that must stay hidden' }); }

writeFileSync('leak-fix-plan.json', JSON.stringify(plan, null, 0));
console.log(JSON.stringify({
  handle_renames_GATED_on_redirect_cap: plan.handle.length,
  tag_removals: plan.tag.length,
  alt_rewrites: plan.alt.length,
  title_rewrites: plan.title.length,
  nicolette_archives: plan.archive.length,
}, null, 2));