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