← back to Designer Wallcoverings
sangetsu: full per-colorway build — 19,463 staged records (Steve: colorway=product), brand=Architectural Wallcoverings, Goodrich scrubbed (0 leaks), 0 banned/Unknown, DWGO, all gated draft (cost+settlement pending); 428 licensed-collection flagged, 99 settlement-risk triaged, 4,218 no-image drafted
7590d2f5b83614898ccb40916112ef59c40822ff · 2026-07-02 09:58:36 -0700 · Steve
Files touched
A onboarding/sangetsu-lilycolor/scripts/sangetsu-build.py
Diff
commit 7590d2f5b83614898ccb40916112ef59c40822ff
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 09:58:36 2026 -0700
sangetsu: full per-colorway build — 19,463 staged records (Steve: colorway=product), brand=Architectural Wallcoverings, Goodrich scrubbed (0 leaks), 0 banned/Unknown, DWGO, all gated draft (cost+settlement pending); 428 licensed-collection flagged, 99 settlement-risk triaged, 4,218 no-image drafted
---
.../sangetsu-lilycolor/scripts/sangetsu-build.py | 190 +++++++++++++++++++++
1 file changed, 190 insertions(+)
diff --git a/onboarding/sangetsu-lilycolor/scripts/sangetsu-build.py b/onboarding/sangetsu-lilycolor/scripts/sangetsu-build.py
new file mode 100644
index 00000000..9aa16bf2
--- /dev/null
+++ b/onboarding/sangetsu-lilycolor/scripts/sangetsu-build.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""sangetsu-build.py — explode Sangetsu pattern-staging into per-colorway
+publishable records (Steve's decision: each colorway = its own product).
+
+OFFLINE / non-gated / idempotent. Reads staging/sangetsu-staging.jsonl (623
+patterns, colorways nested in colorway_skus) and emits one record per UNIQUE
+colorway SKU (~19,463) into staging/sangetsu-unified-staging.jsonl.
+
+HARD CONSTRAINTS (never violated):
+ - Goodrich / Sangetsu NEVER customer-facing. Brand = "Architectural Wallcoverings".
+ Scrub goodrich|sangetsu (+ the raw upstream makers) from title/description/handle.
+ - "Wallpaper" -> "Wallcovering" everywhere. Never "Unknown" in a title.
+ - Every record: cost_confirmed=false, settlement_status="pending" (-> DRAFT),
+ activation_ready=false, status="draft". Publish stays gated on cost + settlement.
+ - vendor_prefix = DWGO (assigned per-unique-SKU, dw_unified write stays GO-B).
+ - Licensed-brand collections (Trussardi/Pip Studio/Missoni/Versace/...) flagged
+ needs_brand_review (trademark/licensing = Steve's call).
+
+Settlement is NOT faked here. All records are settlement_status="pending". A
+labelled, NON-authoritative risk pre-screen (settlement_risk) estimates how many
+patterns will need the real settlement skill, purely so Steve sees the scale.
+"""
+import sys, os, json, re, collections
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+STAGING = os.path.join(os.path.dirname(HERE), "staging")
+IN = os.path.join(STAGING, "sangetsu-staging.jsonl")
+OUT = os.path.join(STAGING, "sangetsu-unified-staging.jsonl")
+
+BRAND = "Architectural Wallcoverings"
+
+# names that must never surface customer-facing (private-label upstream)
+LEAK_RE = re.compile(r"\b(goodrich|sangetsu)\b", re.I)
+# "Wallpaper" -> "Wallcovering" (case-preserving-ish)
+def clean_wallpaper(s):
+ if not s:
+ return s
+ s = re.sub(r"WALLPAPERS", "WALLCOVERINGS", s)
+ s = re.sub(r"WALLPAPER", "WALLCOVERING", s)
+ s = re.sub(r"Wallpapers", "Wallcoverings", s)
+ s = re.sub(r"Wallpaper", "Wallcovering", s)
+ s = re.sub(r"wallpapers", "wallcoverings", s)
+ s = re.sub(r"wallpaper", "wallcovering", s)
+ return s
+
+def scrub_leak(s):
+ if not s:
+ return s
+ return LEAK_RE.sub("", s).replace(" ", " ").strip()
+
+# licensed-brand collections -> needs_brand_review (trademark call)
+LICENSED = re.compile(
+ r"\b(trussardi|pip studio|missoni|versace|zoffany|sanderson|harlequin|"
+ r"morris|william morris|omexco|jab|arte|elitis)\b", re.I)
+
+# settlement RISK pre-screen (TRIAGE ONLY — not the legal gate)
+BOTANICAL = re.compile(
+ r"\b(palm|frond|leaf|leaves|foliage|tropical|jungle|botanical|floral|flower|"
+ r"vine|fern|banana|grape|bird|butterfly|butterflies|feather|nature|garden)\b", re.I)
+
+# junk pattern-name cleanup (entities from the scrape)
+def clean_name(s):
+ if not s:
+ return s
+ s = (s.replace("&", "&").replace("&", "&").replace("&", "&")
+ .replace("’", "'").replace("–", "-").replace("“", "")
+ .replace("”", "").replace(""", '"'))
+ s = re.sub(r"[_]+$", "", s).strip() # trailing underscores from scrape
+ s = re.sub(r"\s+", " ", s)
+ return s
+
+_TITLE_BAD = ("unknown", "page not found", "404", "not found", "undefined", "null")
+
+
+def main():
+ pats = [json.loads(l) for l in open(IN) if l.strip()]
+
+ n_out = 0
+ seen = set()
+ brand_review = 0
+ risk_hi = 0
+ no_img = 0
+ guard_fail = []
+ per_pattern_risk = {}
+
+ with open(OUT, "w") as fout:
+ for p in pats:
+ raw_name = p.get("pattern") or ""
+ name = clean_name(raw_name)
+ overview = clean_wallpaper(scrub_leak(p.get("overview") or "")) or ""
+ spec = p.get("spec") or {}
+ imgs = p.get("sku_images")
+ pat_licensed = bool(LICENSED.search(name))
+ risk = bool(BOTANICAL.search(name + " " + overview))
+ per_pattern_risk[name] = risk
+ if risk:
+ risk_hi += 1
+
+ # normalise image access: dict{sku:url} or list[...]
+ def img_for(sku):
+ if isinstance(imgs, dict):
+ return imgs.get(sku) or (list(imgs.values())[0] if imgs else None)
+ if isinstance(imgs, list) and imgs:
+ return imgs
+ return None
+
+ for x in (p.get("colorway_skus") or []):
+ sku = str(x).strip()
+ if not sku or sku in seen:
+ continue
+ seen.add(sku)
+
+ img = img_for(sku)
+ has_image = bool(img)
+ if not has_image:
+ no_img += 1
+
+ # title: "<Pattern> <ColorwaySKU> | Architectural Wallcoverings"
+ # (no per-colorway color name exists in the free source -> flag it)
+ pat_disp = name if name else sku # never blank; never "Unknown"
+ title = clean_wallpaper(scrub_leak(f"{pat_disp} {sku} | {BRAND}"))
+ # Guard: "wallpaper" banned anywhere; error-tokens only as WHOLE
+ # words in the PATTERN-NAME segment — NOT substring-matched into the
+ # SKU (e.g. "404" legitimately appears in KAM404 / MIN3404).
+ nameseg = pat_disp.lower()
+ bad = "wallpaper" in title.lower() or any(
+ re.search(r"(?<![0-9a-z])" + re.escape(t) + r"(?![0-9a-z])", nameseg)
+ for t in _TITLE_BAD)
+ if bad:
+ guard_fail.append((sku, title))
+ continue # never emit a bad title
+
+ rec = {
+ "source": "goodrich-sangetsu", # internal only, never shipped
+ "mfr_sku": sku,
+ "mfr_prefix": p.get("mfr_prefix") or "",
+ "pattern": pat_disp,
+ "book": pat_disp,
+ "vendor_prefix": "DWGO",
+ "brand": BRAND,
+ "title": title,
+ "description": overview,
+ "width": spec.get("width") or "",
+ "spec": spec,
+ "image_url": img,
+ "image_count": 1 if has_image else 0,
+ "source_url": p.get("source_url"),
+ # gates — all closed; publish needs cost AND settlement
+ "cost_confirmed": False,
+ "settlement_status": "pending",
+ "settlement_risk_triage": "review" if risk else "low",
+ "activation_ready": False,
+ "status": "draft",
+ "needs_color_name": True,
+ "needs_brand_review": pat_licensed,
+ "needs_settlement": True,
+ }
+ if pat_licensed:
+ brand_review += 1
+ fout.write(json.dumps(rec, ensure_ascii=False) + "\n")
+ n_out += 1
+
+ # leak audit on the emitted file (belt + suspenders)
+ leaks = 0
+ banned = 0
+ for l in open(OUT):
+ d = json.loads(l)
+ blob = (d["title"] + " " + d["description"]).lower()
+ if re.search(r"goodrich|sangetsu", blob):
+ leaks += 1
+ if re.search(r"\bwallpaper", blob):
+ banned += 1
+
+ print(json.dumps({
+ "patterns_in": len(pats),
+ "colorway_records_out": n_out,
+ "title_guard_blocked": len(guard_fail),
+ "records_no_image (-> DRAFT)": no_img,
+ "needs_brand_review (licensed collections)": brand_review,
+ "settlement_risk_triage patterns (NON-authoritative)": risk_hi,
+ "LEAK audit goodrich/sangetsu in title+desc": leaks,
+ "BANNED 'wallpaper' in title+desc": banned,
+ "out": os.path.relpath(OUT, os.path.dirname(HERE)),
+ }, indent=2))
+ if guard_fail:
+ print(" sample guard-blocked:", guard_fail[:3])
+
+
+if __name__ == "__main__":
+ main()
← 0e30504b discontinued-sweep: report generator + README; limited-stock
·
back to Designer Wallcoverings
·
Read-only cross-brand single-roll min/step audit: 15,034 var fdd0045b →