← back to Tk10630 Sku Suffix Canary

leak-assess-accurate.mjs

41 lines

// Accurate private-label leak assessment — validates matches as WHOLE TOKENS
// (handles/titles use hyphen/space separators), eliminating substring false
// positives like versailles/versace for 'versa'. READ-ONLY.
import { readFileSync } 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 NOISY = new Set(['york', 'yorkwall']);
const terms = [...new Set(denylist.terms.map(t => t.term.toLowerCase().split(/\s+/)[0]).filter(t => t.length >= 4 && !NOISY.has(t)))];
const mapsTo = Object.fromEntries(denylist.terms.map(t => [t.term.toLowerCase().split(/\s+/)[0], t.maps_to]));

// whole-token: term bounded by start/end or a non-alphanumeric (hyphen/space)
const tok = term => new RegExp(`(^|[^a-z0-9])${term}([^a-z0-9]|$)`, 'i');

const rows = [];
for (const term of terms) {
  const re = tok(term);
  // fetch all substring-matching active products (bounded), then validate whole-token
  const seen = new Map();
  for (const field of ['handle', 'title', 'tag']) {
    let cursor = null, guard = 0;
    while (guard++ < 20) {
      const d = 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:2){ nodes{ altText } } } } }`);
      for (const p of d.data.products.nodes) if (!seen.has(p.id)) seen.set(p.id, p);
      if (!d.data.products.pageInfo.hasNextPage) break;
      cursor = d.data.products.pageInfo.endCursor;
    }
  }
  let h = 0, t = 0, g = 0, alt = 0;
  for (const p of seen.values()) {
    if (re.test(p.handle || '')) h++;
    if (re.test(p.title || '')) t++;
    if ((p.tags || []).some(x => re.test(x))) g++;
    if (p.images.nodes.some(i => re.test(i.altText || ''))) alt++;
  }
  if (h || t || g || alt) rows.push({ term, maps_to: mapsTo[term], handle: h, title: t, tag: g, alt, distinct_products: seen.size });
}
rows.sort((a, b) => (b.handle + b.title + b.tag + b.alt) - (a.handle + a.title + a.tag + a.alt));
console.log(JSON.stringify({ terms_checked: terms.length, real_leaking_terms: rows.length, rows }, null, 2));
console.log(`\nREAL active handle leaks (whole-token): ${rows.reduce((a, r) => a + r.handle, 0)}`);