← 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
M leak-scanner-api-handle-alt-scan.mjs
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 →