[object Object]

← back to Tk10630 Sku Suffix Canary

leak-scanner: full-phrase matching (nicolette mayer/lillian august not the bare name); leak surface -> 1 real product

0ee723d447c51a7d371e11b9a78032893f51d332 · 2026-08-17 16:02:46 -0700 · steve

Files touched

Diff

commit 0ee723d447c51a7d371e11b9a78032893f51d332
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Aug 17 16:02:46 2026 -0700

    leak-scanner: full-phrase matching (nicolette mayer/lillian august not the bare name); leak surface -> 1 real product
---
 leak-scanner-api-handle-alt-scan.mjs | 27 +++++++++++++++++----------
 1 file changed, 17 insertions(+), 10 deletions(-)

diff --git a/leak-scanner-api-handle-alt-scan.mjs b/leak-scanner-api-handle-alt-scan.mjs
index 868c0c4..5937a3b 100644
--- a/leak-scanner-api-handle-alt-scan.mjs
+++ b/leak-scanner-api-handle-alt-scan.mjs
@@ -6,11 +6,18 @@ 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]));
+// 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-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');
+// 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
@@ -32,13 +39,13 @@ const correlates = (vendor, term) => {
 };
 
 const rows = [];
-for (const term of terms) {
-  const re = tok(term);
+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}:*${term}* status:ACTIVE", after:$c){ pageInfo{ hasNextPage endCursor } nodes{ id handle title vendor tags images(first:2){ nodes{ altText } } } } }`);
+      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;
@@ -47,16 +54,16 @@ for (const term of terms) {
   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, term)) { falsePos++; continue; } // legit pattern/place name on another vendor
+    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, maps_to: mapsTo[term], handle: h, title: t, tag: g, alt, false_positives_uncorrelated: falsePos });
+  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: terms.length, real_leaking_terms: rows.length, rows }, null, 2));
+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

← a378384 yoloforever cycle3: generator-redesign design doc + Cody-gat  ·  back to Tk10630 Sku Suffix Canary  ·  generator design: Option C uniqueness verified (0 collisions 3c841bc →