← 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
M .gitignoreM lib/export-catalog.shM merge-work/leak-scan.py
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 →