← back to Tk10630 Sku Suffix Canary
leak-scanner-api-handle-alt-scan.mjs
72 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']);
// Each term keeps its FULL phrase for validation (a multi-word brand like "nicolette
// mayer"/"lillian august" must match the WHOLE phrase — matching just "nicolette"/"lillian"
// over-matches common names used as pattern names). searchWord = first word (for the
// Shopify wildcard search only); the whole-PHRASE regex is what actually confirms a leak.
const entries = denylist.terms
.map(t => ({ phrase: t.term.toLowerCase().trim(), searchWord: t.term.toLowerCase().trim().split(/\s+/)[0], maps_to: t.maps_to }))
.filter(e => e.searchWord.length >= 4 && !NOISY.has(e.searchWord));
const mapsTo = Object.fromEntries(entries.map(e => [e.phrase, e.maps_to]));
// whole-PHRASE: match the full phrase (spaces flex to hyphen/space/underscore between
// words, as handles use), bounded so "nicolette" alone or "lilliana" don't match.
const phraseRe = phrase => new RegExp(`(^|[^a-z0-9])${phrase.split(/\s+/).join('[-_ ]+')}([^a-z0-9]|$)`, 'i');
// VENDOR CORRELATION (2026-08-17, Cody-gated): a term only counts as a LEAK when it
// appears on a product whose PUBLIC vendor is the label it maps to. Otherwise it's a
// legit place/pattern name (Chesapeake color, Brewster town, Momentum pattern) on a
// different vendor — NOT a private-label leak. Without this the scanner false-positives.
// base public brand for a term, or '' if the term has no vendor mapping (e.g. an
// ARCHIVED brand like "nicolette mayer" -> "(archived — must stay hidden)").
const vendorOf = term => { const m = (mapsTo[term] || ''); if (/^\(?archived/i.test(m.trim())) return ''; return m.split('(')[0].trim().toLowerCase(); };
// Correlation is a FALSE-POSITIVE FILTER, not a global gate (Cody, Cycle 2): only
// EXCLUDE a match when the product is clearly a DIFFERENT real vendor. Terms with no
// vendor mapping (archived brands) and blank-vendor products ALWAYS report — never
// silence an archived-brand or missing-vendor hit.
const correlates = (vendor, term) => {
const b = vendorOf(term);
if (!b) return true; // no mapping (archived) -> always report
const v = (vendor || '').trim().toLowerCase();
if (!v) return true; // blank vendor -> can't rule out -> report
return v === b || v.includes(b) || b.includes(v);
};
const rows = [];
for (const e of entries) {
const re = phraseRe(e.phrase);
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}:*${e.searchWord}* status:ACTIVE", after:$c){ pageInfo{ hasNextPage endCursor } nodes{ id handle title vendor 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, falsePos = 0;
for (const p of seen.values()) {
if (!(re.test(p.handle || '') || re.test(p.title || '') || (p.tags || []).some(x => re.test(x)) || p.images.nodes.some(i => re.test(i.altText || '')))) continue;
if (!correlates(p.vendor, e.phrase)) { falsePos++; continue; } // legit pattern/place name on another vendor
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: e.phrase, maps_to: e.maps_to, handle: h, title: t, tag: g, alt, false_positives_uncorrelated: falsePos });
}
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: entries.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)}`);
// verdict for run.sh / fleet-health
const verdict = rows.some(r => r.handle || r.alt || r.title) ? 'FAIL' : (rows.length ? 'WARN' : 'PASS');
console.log(`\n[api-handle-alt-scan] verdict=${verdict} (customer-facing leaks in handle/title/alt = FAIL; tag-only = WARN). Whole-token validated — no versailles/versace false positives.`);