[object Object]

← back to Dw Vendor Microsites

scrub-gate: add New York place-name FP guard to export-catalog + leak-scan

eb61eab668772a2a1393a0696f15f4909da1c6f7 · 2026-07-01 10:03:46 -0700 · Steve

The malibu/phillipe_romano/hollywood private-label houses legitimately ban
"York" (they source from the real York/Brewster). The shared shell/python
scrub gate did word-boundary York matching with NO place-name guard, so a
re-export of those houses would corrupt legit "New York" NYC pattern names
(Concrete New York -> Concrete New MalibuWallpaper) and leak-scan would
false-flag them. dw-leak-scanner/leak-match.mjs already guards this class;
this ports the same guard to the microsites gate.

Guard (armed only when a bare "york" is in the banned set): protects
new york / nueva york / york weave / york-scrape from the scrub+scan, while
still de-branding a standalone "York" / "yorkwall". Verified: rebel_walls
+ designers_guild go clean under a hypothetical York ban; york_contract's 266
genuine York leaks still flag; genuine 'York Wallcoverings'/'yorkwall.com' URLs
still scrubbed.

Rebel Walls itself is a public brand with no banned list, so it is never
scrubbed and had zero genuine leaks — the canary flag was a stale false
positive already resolved upstream by leak-match.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit eb61eab668772a2a1393a0696f15f4909da1c6f7
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 10:03:46 2026 -0700

    scrub-gate: add New York place-name FP guard to export-catalog + leak-scan
    
    The malibu/phillipe_romano/hollywood private-label houses legitimately ban
    "York" (they source from the real York/Brewster). The shared shell/python
    scrub gate did word-boundary York matching with NO place-name guard, so a
    re-export of those houses would corrupt legit "New York" NYC pattern names
    (Concrete New York -> Concrete New MalibuWallpaper) and leak-scan would
    false-flag them. dw-leak-scanner/leak-match.mjs already guards this class;
    this ports the same guard to the microsites gate.
    
    Guard (armed only when a bare "york" is in the banned set): protects
    new york / nueva york / york weave / york-scrape from the scrub+scan, while
    still de-branding a standalone "York" / "yorkwall". Verified: rebel_walls
    + designers_guild go clean under a hypothetical York ban; york_contract's 266
    genuine York leaks still flag; genuine 'York Wallcoverings'/'yorkwall.com' URLs
    still scrubbed.
    
    Rebel Walls itself is a public brand with no banned list, so it is never
    scrubbed and had zero genuine leaks — the canary flag was a stale false
    positive already resolved upstream by leak-match.mjs.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .gitignore              |  3 +++
 lib/export-catalog.sh   | 31 +++++++++++++++++++++++++++++--
 merge-work/leak-scan.py | 35 ++++++++++++++++++++++++++++++++---
 3 files changed, 64 insertions(+), 5 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0d9ca1d..8edc736 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ data/sample-requests.jsonl
 vendors/*/viewer/data/sample-requests.jsonl
 # NOTE: staging/*.jsonl is intentionally KEPT in git (the exported, scrubbed catalog
 # snapshots are the resumable build inputs — OK to keep, per plan).
+
+__pycache__/
+*.pyc
diff --git a/lib/export-catalog.sh b/lib/export-catalog.sh
index b47d641..ca10dd9 100755
--- a/lib/export-catalog.sh
+++ b/lib/export-catalog.sh
@@ -56,6 +56,30 @@ text_pats = [re.compile(r'\b' + re.escape(b) + r'\b', re.IGNORECASE) for b in ba
 # like "momentumtextilesandwalls" have no word boundaries, so use substring match
 banned_tokens = [re.sub(r'[^a-z0-9]', '', b.lower()) for b in banned_sorted if re.sub(r'[^a-z0-9]', '', b.lower())]
 
+# ---- place-name FALSE-POSITIVE guard (mirrors dw-leak-scanner/leak-match.mjs) --
+# The malibu/phillipe_romano/hollywood houses source from the real York/Brewster
+# and legitimately ban "York". A bare "york" ban must NOT corrupt "New York" NYC
+# place names ("Concrete New York" -> "Concrete New MalibuWallpaper"), the "York
+# Weave" pattern, or the internal "york-scrape" provenance token — it must still
+# de-brand a standalone "York" / "yorkwall". We PROTECT those phrases by masking
+# them to a sentinel before the banned-word scrub runs, then RESTORE them after.
+# Guard is armed ONLY when a bare "york" is in the banned set; no-op otherwise.
+_york_guard = any(re.sub(r'[^a-z0-9]', '', b.lower()) == 'york' for b in banned_sorted)
+_YORK_PROTECT = re.compile(r'new york|nueva york|york weave|york[-_]scrape', re.IGNORECASE)
+_YORK_SENTINEL = '{}'  # private-use chars, never appear in catalog text
+_york_saved = []
+def protect_york(s):
+    if not _york_guard or not isinstance(s, str) or not s:
+        return s
+    def _mask(m):
+        _york_saved.append(m.group(0))
+        return _YORK_SENTINEL.format(len(_york_saved) - 1)
+    return _YORK_PROTECT.sub(_mask, s)
+def restore_york(s):
+    if not _york_guard or not isinstance(s, str) or '' not in s:
+        return s
+    return re.sub(r'(\d+)', lambda m: _york_saved[int(m.group(1))], s)
+
 DROP_KEYS = {'product_url', 'product_link', 'source_url', 'vendor_url'}
 # a value/key is treated as image/url/path-like if its KEY hints at it OR the
 # value itself looks like a URL or file path
@@ -84,8 +108,11 @@ def scrub_value(key, v):
     if isinstance(v, str):
         if not banned_tokens or not v:
             return v
+        # place-name FP guard: mask "New York"/"York Weave"/"york-scrape" so the
+        # bare-"york" ban can't corrupt or blank them; restore before returning.
+        v = protect_york(v)
         if is_urlish(key, v):
-            return '' if has_banned_token(v) else v
+            return '' if has_banned_token(v) else restore_york(v)
         # plain text -> word-boundary replace; but if a banned token is still
         # embedded (e.g. a glued slug not caught by \b) and the string is a slug
         # with no spaces, blank it as a leak vector rather than leaving it.
@@ -113,7 +140,7 @@ def scrub_value(key, v):
                     gp = re.compile(r'(?<![A-Za-z])' + re.escape(tok) + r'(?![A-Za-z])', re.IGNORECASE)
                     out = gp.sub(house, out)
                 out = re.sub(r'\s{2,}', ' ', out).strip(' -|,/')
-        return out
+        return restore_york(out)
     if isinstance(v, list):
         cleaned = [scrub_value(key, x) for x in v]
         # for url-ish list keys, drop emptied entries
diff --git a/merge-work/leak-scan.py b/merge-work/leak-scan.py
index af03809..9f291e3 100644
--- a/merge-work/leak-scan.py
+++ b/merge-work/leak-scan.py
@@ -10,15 +10,42 @@ wpats = [(b, re.compile(r'\b' + re.escape(b) + r'\b', re.IGNORECASE)) for b in b
 toks  = [(b, re.sub(r'[^a-z0-9]', '', b.lower())) for b in banned]
 toks  = [(b, t) for (b, t) in toks if t]
 
+# ---- place-name FALSE-POSITIVE guard (mirrors dw-leak-scanner/leak-match.mjs) ----
+# A bare "York" ban (the malibu/phillipe_romano/hollywood houses source from the
+# real York/Brewster and legitimately ban "York") must NOT flag "New York" NYC
+# place names ("Concrete New York"), the "York Weave" pattern, or the internal
+# "york-scrape"/"york_scrape" provenance token. It DOES still flag a standalone
+# "York" brand form or "yorkwall". Only the bare-"york" term is guarded; all
+# other banned brands (Brewster, WallQuest, …) have no place-name FP risk.
+_YORK_STRIP_TEXT = re.compile(r'new york|nueva york|york weave', re.IGNORECASE)
+_YORK_STRIP_TOK  = re.compile(r'newyork|nuevayork|yorkweave|yorkscrape', re.IGNORECASE)
+_YORK_WORD       = re.compile(r'\byork\b', re.IGNORECASE)
+_YORK_TOK        = re.compile(r'york')
+
+def _is_bare_york(b):
+    return re.sub(r'[^a-z0-9]', '', b.lower()) == 'york'
+
+def _york_survives_text(s):
+    # strip the known place/pattern phrases, then re-test for a standalone York
+    return bool(_YORK_WORD.search(_YORK_STRIP_TEXT.sub(' ', s)))
+
+def _york_survives_tok(low):
+    # low is already alnum-only lowercase; strip glued place forms then re-test
+    return bool(_YORK_TOK.search(_YORK_STRIP_TOK.sub('', low)))
+
 def check_text(s):
     for b, p in wpats:
-        if p.search(s): return b
+        if p.search(s):
+            if _is_bare_york(b) and not _york_survives_text(s): continue
+            return b
     return None
 
 def check_urlish(s):
     low = re.sub(r'[^a-z0-9]', '', s.lower())
     for b, t in toks:
-        if t in low: return b
+        if t in low:
+            if _is_bare_york(b) and not _york_survives_tok(low): continue
+            return b
     return None
 
 def is_slug_or_url(s):
@@ -38,7 +65,9 @@ def scan(line):
             for k, v in o.items():
                 kt = re.sub(r'[^a-z0-9]', '', str(k).lower())
                 for b, t in toks:
-                    if t and t in kt: return b
+                    if t and t in kt:
+                        if _is_bare_york(b) and not _york_survives_tok(kt): continue
+                        return b
                 stack.append(v)
         elif isinstance(o, list):
             stack.extend(o)

← c5760b0 fix vendor-viewer port collisions: align manifest to live/ng  ·  back to Dw Vendor Microsites  ·  auto-save: 2026-07-07T13:30:45 (2 files) — staging/pierre_fr 1ce3846 →