← back to Dw Pairs Well
Add pattern-id-backfill dry-run (Option-A hybrid) + texture-vision stub
90dbcafb700cbbf0e0695d57c8ac78fe70c473d0 · 2026-06-25 23:58:33 -0700 · Steve Abrams
- tools/pattern-id-backfill.js: DRY-RUN-ONLY pattern_id computer.
mfr_root primary (input map or --use-mirror-mfr) -> T2 title fallback ->
pattern_name last resort. Texture-aware bucketing: skip image gate only for
texture+mfr_root non-huge groups; texture T2/huge -> local-vision; non-texture
-> dHash. --apply is a GATED no-op stub (writes nothing).
- tools/pattern-texture-vision-stub.py: design-only Ollama qwen2.5vl helper ($0).
- Finding: mirror mfr_sku column is polluted (SANCAR/TRUE junk roots) -> validates
DTD that the authoritative source is the gated custom.manufacturer_sku metafield.
Files touched
A tools/grouping-demo.htmlA tools/pattern-grouping-demo.pyA tools/pattern-id-backfill-report.jsonA tools/pattern-id-backfill.jsA tools/pattern-texture-vision-stub.py
Diff
commit 90dbcafb700cbbf0e0695d57c8ac78fe70c473d0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 25 23:58:33 2026 -0700
Add pattern-id-backfill dry-run (Option-A hybrid) + texture-vision stub
- tools/pattern-id-backfill.js: DRY-RUN-ONLY pattern_id computer.
mfr_root primary (input map or --use-mirror-mfr) -> T2 title fallback ->
pattern_name last resort. Texture-aware bucketing: skip image gate only for
texture+mfr_root non-huge groups; texture T2/huge -> local-vision; non-texture
-> dHash. --apply is a GATED no-op stub (writes nothing).
- tools/pattern-texture-vision-stub.py: design-only Ollama qwen2.5vl helper ($0).
- Finding: mirror mfr_sku column is polluted (SANCAR/TRUE junk roots) -> validates
DTD that the authoritative source is the gated custom.manufacturer_sku metafield.
---
tools/grouping-demo.html | 174 ++++++++++++++
tools/pattern-grouping-demo.py | 204 +++++++++++++++++
tools/pattern-id-backfill-report.json | 417 ++++++++++++++++++++++++++++++++++
tools/pattern-id-backfill.js | 308 +++++++++++++++++++++++++
tools/pattern-texture-vision-stub.py | 54 +++++
5 files changed, 1157 insertions(+)
diff --git a/tools/grouping-demo.html b/tools/grouping-demo.html
new file mode 100644
index 0000000..a9e4a69
--- /dev/null
+++ b/tools/grouping-demo.html
@@ -0,0 +1,174 @@
+<!doctype html><meta charset=utf-8>
+<title>DW · Collapse colorways — visual proof</title>
+<style>
+ body{font:15px/1.5 -apple-system,Segoe UI,sans-serif;margin:0;background:#faf8f5;color:#1a1a1a}
+ header{padding:22px 28px;background:#111;color:#fff}
+ header h1{margin:0;font-size:19px;font-weight:600;letter-spacing:.02em}
+ header p{margin:6px 0 0;color:#bbb;font-size:13px}
+ .wrap{padding:24px 28px;max-width:1280px;margin:0 auto}
+ h2{font:600 15px/1 sans-serif;text-transform:uppercase;letter-spacing:.08em;color:#666;margin:34px 0 14px}
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:18px}
+ .tile{background:#fff;border:1px solid #e7e2da;border-radius:8px;overflow:hidden}
+ .tile .hero{width:100%;aspect-ratio:1;object-fit:cover;display:block}
+ .meta{padding:11px 13px}
+ .pname{font-weight:600;font-size:14px}
+ .ven{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:#888;margin:2px 0 9px}
+ .chip{display:inline-flex;align-items:center;gap:7px;border:1px solid #d8d2c8;background:#fff;
+ border-radius:999px;padding:5px 12px;font-size:13px;font-weight:600;cursor:pointer}
+ .chip .dots{display:inline-flex}
+ .chip .dots i{width:15px;height:15px;border-radius:50%;background:#cabfa9;border:2px solid #fff;margin-right:-6px;box-shadow:0 0 0 1px #d8d2c8}
+ .chip .car{transition:transform .2s}
+ .tile.open .chip .car{transform:rotate(180deg)}
+ .note{font-size:11px;margin-top:8px}
+ .ways{display:none;flex-wrap:wrap;gap:8px;padding:4px 13px 14px}
+ .tile.open .ways{display:flex}
+ .cw{display:block;text-decoration:none;color:#444;text-align:center}
+ .cw img{object-fit:cover;border-radius:5px;border:1px solid #e7e2da;display:block}
+ .cw span{display:block;font-size:9px;max-width:92px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:3px;color:#888}
+ .diag{display:flex;flex-wrap:wrap;gap:14px}
+ .dcl{border:1px dashed #c9b; border-radius:8px;padding:8px;background:#fff}
+ .dcl small{display:block;text-align:center;color:#9a2b9a;font-size:10px;margin-top:4px;text-transform:uppercase}
+ .legend{font-size:12px;color:#555;background:#fff;border:1px solid #e7e2da;border-radius:8px;padding:12px 16px}
+</style>
+<header>
+ <h1>Designer Wallcoverings — collapse colorways into one pattern tile</h1>
+ <p>Real catalog data · each tile = one pattern · “N Colors” expands to the colorways · grouping confirmed by the color-invariant image check (dHash≤16, $0 local)</p>
+</header>
+<div class="wrap">
+ <div class="legend">Click a <b>“N Colors ⌄”</b> chip to expand the confirmed colorways. The coloured dot under each tile reports the image-confirmation status:
+ <span style="color:#1c7c3c">● confirmed</span> · <span style="color:#8a6d00">● mostly (outliers split off)</span> · <span style="color:#9a2b2b">● mixed</span>.</div>
+ <h2>Collection grid — after (one tile per pattern)</h2>
+ <div class="grid">
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/LINSEY_WOOLSEY_SE21-21.jpg?v=1751951871" loading="lazy">
+ <div class="meta">
+ <div class="pname">Linsey Pvc Free Type Ii Linsey Pvc</div>
+ <div class="ven">Koroseal</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i></span>
+ 1 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#9a2b2b">● 1 confirmed · 13 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/DWK-30461" title="Linsey Pvc Free Type Ii Linsey Pvc-free Type Ii Color 21 / Architectural Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/LINSEY_WOOLSEY_SE21-21.jpg?v=1751951871" style="width:92px;height:92px"><span>Linsey Pvc Free Type Ii Linsey Pvc-free Type Ii Color 21 / Architectural Wallcoverings</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-21_9c24c501-a508-4906-ad9f-7bf9d7d05b8b.jpg?v=1733889981" loading="lazy">
+ <div class="meta">
+ <div class="pname">Amherst</div>
+ <div class="ven">Phillipe Romano</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 9 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#8a6d00">● 9 confirmed · 5 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/XXP-969784" title="Amherst-Fresh Mint Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-21_9c24c501-a508-4906-ad9f-7bf9d7d05b8b.jpg?v=1733889981" style="width:92px;height:92px"><span>Amherst-Fresh Mint Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969785" title="Amherst-Sun Shower Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-22_bc6cf90a-f6da-48dd-ae6a-817b3c900ba4.jpg?v=1733889979" style="width:92px;height:92px"><span>Amherst-Sun Shower Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969786" title="Amherst-Pale Petal Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-23_c6c6fdf4-9f79-4920-88c8-c9b203ea1a8c.jpg?v=1733889976" style="width:92px;height:92px"><span>Amherst-Pale Petal Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969787" title="Amherst-Lilac Tint Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-24_fa93ac36-cd7b-4bfa-9a25-03441ba10d78.jpg?v=1733889973" style="width:92px;height:92px"><span>Amherst-Lilac Tint Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969768" title="Amherst-Dove Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-05_5fc29b5d-6a47-4960-b67e-98d5ddf57aee.jpg?v=1733890021" style="width:92px;height:92px"><span>Amherst-Dove Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969775" title="Amherst-Black Walnut Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-12_678e944a-3725-495b-82fe-c38297936bf8.jpg?v=1733890004" style="width:92px;height:92px"><span>Amherst-Black Walnut Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969781" title="Amherst-Burgundy Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-18_3c6f3f7b-bd63-4054-af92-5a12d8666bb1.jpg?v=1733889989" style="width:92px;height:92px"><span>Amherst-Burgundy Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969782" title="Amherst-Olive Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-19_b4fa4203-6e61-4694-b4ce-f4a2d3340f06.jpg?v=1733889987" style="width:92px;height:92px"><span>Amherst-Olive Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969773" title="Amherst-Fawn Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-WN-10_89d23667-aac6-49ee-9fad-8cf48067223f.jpg?v=1733890009" style="width:92px;height:92px"><span>Amherst-Fawn Durable Vinyl / Phillipe Romano</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/grv-2885_8.jpg?v=1762295987" loading="lazy">
+ <div class="meta">
+ <div class="pname">Grove</div>
+ <div class="ven">Wolf Gordon</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i></span>
+ 2 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#9a2b2b">● 2 confirmed · 12 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/DWWG-GRV-2885_8" title="Grove - Silver / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/grv-2885_8.jpg?v=1762295987" style="width:92px;height:92px"><span>Grove - Silver / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-GRV-2902_8" title="Grove - Nickel / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/grv-2902_8.jpg?v=1762296600" style="width:92px;height:92px"><span>Grove - Nickel / Wolf Gordon Wallcoverings</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/wwdf-216f.jpg?v=1762312608" loading="lazy">
+ <div class="meta">
+ <div class="pname">Wonderwood</div>
+ <div class="ven">Wolf Gordon</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 4 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#9a2b2b">● 4 confirmed · 10 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/DWWG-WWDF-216_8" title="WonderWood® - Barn Fumed Oak / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/wwdf-216f.jpg?v=1762312608" style="width:92px;height:92px"><span>WonderWood® - Barn Fumed Oak / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-WWDF-209" title="WonderWood® - Australian, Walnut Qtd / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/wwdf-209f.jpg?v=1762312375" style="width:92px;height:92px"><span>WonderWood® - Australian, Walnut Qtd / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-WWDF-208" title="WonderWood® - White Oak, Rift / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/wwdf-208.jpg?v=1762312341" style="width:92px;height:92px"><span>WonderWood® - White Oak, Rift / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-WWDF-200" title="WonderWood® - Rosewood East Indian Qtd / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/wwdf-200f.jpg?v=1762312088" style="width:92px;height:92px"><span>WonderWood® - Rosewood East Indian Qtd / Wolf Gordon Wallcoverings</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-LS-01_a90bf60c-8954-4fa2-8550-5ca9ecdbd191.jpg?v=1733890788" loading="lazy">
+ <div class="meta">
+ <div class="pname">Dover</div>
+ <div class="ven">Phillipe Romano</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i></span>
+ 1 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#9a2b2b">● 1 confirmed · 13 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/XXP-969488" title="Dover-Frosty White Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-LS-01_a90bf60c-8954-4fa2-8550-5ca9ecdbd191.jpg?v=1733890788" style="width:92px;height:92px"><span>Dover-Frosty White Durable Vinyl / Phillipe Romano</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-21_d3e1e583-0f99-419c-8b1d-9ee3ec7d72e8.jpg?v=1733890489" loading="lazy">
+ <div class="meta">
+ <div class="pname">Claremont Silk</div>
+ <div class="ven">Phillipe Romano</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 14 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#1c7c3c">● image-confirmed colorways</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/XXP-969591" title="Claremont Silk-Andesite Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-21_d3e1e583-0f99-419c-8b1d-9ee3ec7d72e8.jpg?v=1733890489" style="width:92px;height:92px"><span>Claremont Silk-Andesite Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969594" title="Claremont Silk-Travertine Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-24_815a960b-4ba9-44ae-9acc-03e754af2749.jpg?v=1733890480" style="width:92px;height:92px"><span>Claremont Silk-Travertine Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969577" title="Claremont Silk-Pearl Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-05_4c1a44d6-e75d-4beb-9aa8-bd4e7b35bb1e.jpg?v=1733890534" style="width:92px;height:92px"><span>Claremont Silk-Pearl Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969592" title="Claremont Silk-Onyx Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-22_4f37b39d-aae0-4769-ab5e-817f0d253d3d.jpg?v=1733890486" style="width:92px;height:92px"><span>Claremont Silk-Onyx Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969587" title="Claremont Silk-Emerald Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-16_64811d2d-8157-4fab-ab7c-2246bc75f12f.jpg?v=1733890501" style="width:92px;height:92px"><span>Claremont Silk-Emerald Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969583" title="Claremont Silk-Golden Beryl Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-11_58f14fd1-4807-4f40-852c-b02c8a73596a.jpg?v=1733890516" style="width:92px;height:92px"><span>Claremont Silk-Golden Beryl Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969581" title="Claremont Silk-Lemon Quartz Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-09_eb411c57-29bb-4fd4-a125-4011c97668e4.jpg?v=1733890522" style="width:92px;height:92px"><span>Claremont Silk-Lemon Quartz Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969574" title="Claremont Silk-Grey Star Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-02_b471a388-33c8-4bf3-bcf4-f4da7e6e1fc2.jpg?v=1733890543" style="width:92px;height:92px"><span>Claremont Silk-Grey Star Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969595" title="Claremont Silk-Carnelian Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-25_9c56870a-460c-4c8a-85e7-f7a3fdd85d45.jpg?v=1733890478" style="width:92px;height:92px"><span>Claremont Silk-Carnelian Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969580" title="Claremont Silk-Rock Crystal Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-08_d3f4a189-aec5-4244-8b29-90a7ee8d7a39.jpg?v=1733890525" style="width:92px;height:92px"><span>Claremont Silk-Rock Crystal Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969585" title="Claremont Silk-Silver Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-13_d6fecbcb-79bd-40cd-91ea-0d062d36528c.jpg?v=1733890509" style="width:92px;height:92px"><span>Claremont Silk-Silver Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969589" title="Claremont Silk-Amazonite Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-18_3cb7e2d9-605a-479c-b2d6-bce04ba26f05.jpg?v=1733890495" style="width:92px;height:92px"><span>Claremont Silk-Amazonite Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969582" title="Claremont Silk-Calcite Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-10_596ebaac-139c-40ac-ada4-8c435641cb72.jpg?v=1733890519" style="width:92px;height:92px"><span>Claremont Silk-Calcite Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969579" title="Claremont Silk-Riverstone Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-MS-07_c34ab3c0-e0d7-43e8-91eb-ade892d41c53.jpg?v=1733890528" style="width:92px;height:92px"><span>Claremont Silk-Riverstone Durable Vinyl / Phillipe Romano</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5050.jpg?v=1762292093" loading="lazy">
+ <div class="meta">
+ <div class="pname">Dasma</div>
+ <div class="ven">Wolf Gordon</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 10 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#8a6d00">● 10 confirmed · 4 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5050" title="Dasma - Slate / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5050.jpg?v=1762292093" style="width:92px;height:92px"><span>Dasma - Slate / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5048" title="Dasma - Nickel / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5048.jpg?v=1762292020" style="width:92px;height:92px"><span>Dasma - Nickel / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5041" title="Dasma - Parchment / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5041.jpg?v=1762291757" style="width:92px;height:92px"><span>Dasma - Parchment / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5045" title="Dasma - Bronze / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5045.jpg?v=1762291910" style="width:92px;height:92px"><span>Dasma - Bronze / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5049" title="Dasma - Aquamarine / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5049.jpg?v=1762292057" style="width:92px;height:92px"><span>Dasma - Aquamarine / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5043" title="Dasma - Taupe / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5043.jpg?v=1762291835" style="width:92px;height:92px"><span>Dasma - Taupe / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5053" title="Dasma - Midnight / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5053.jpg?v=1762292207" style="width:92px;height:92px"><span>Dasma - Midnight / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5047" title="Dasma - Silver / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5047_50412f01-1807-46cc-9514-4d1f10afa98d.jpg?v=1762291983" style="width:92px;height:92px"><span>Dasma - Silver / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5038" title="Dasma - Buff / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5038.jpg?v=1762291641" style="width:92px;height:92px"><span>Dasma - Buff / Wolf Gordon Wallcoverings</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWWG-DSM-5040" title="Dasma - Fawn / Wolf Gordon Wallcoverings"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/dsm-5040.jpg?v=1762291717" style="width:92px;height:92px"><span>Dasma - Fawn / Wolf Gordon Wallcoverings</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-11_88e6d082-af1a-4d89-a2f9-b37540da70c0.jpg?v=1733891560" loading="lazy">
+ <div class="meta">
+ <div class="pname">Troy</div>
+ <div class="ven">Phillipe Romano</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 14 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#1c7c3c">● image-confirmed colorways</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/XXP-969151" title="Troy-Mystify Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-11_88e6d082-af1a-4d89-a2f9-b37540da70c0.jpg?v=1733891560" style="width:92px;height:92px"><span>Troy-Mystify Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969156" title="Troy-Pickle Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-22_073ad733-81e9-431a-810e-03d09b807af4.jpg?v=1733891547" style="width:92px;height:92px"><span>Troy-Pickle Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969161" title="Troy-Silver Secret Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-27_4e12cf05-97b0-47a5-a998-6aa1edb6e174.jpg?v=1733891536" style="width:92px;height:92px"><span>Troy-Silver Secret Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969149" title="Troy-Enigma Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-04_eaec5d38-cd23-43e2-ba37-60e73bc9e7d5.jpg?v=1733891564" style="width:92px;height:92px"><span>Troy-Enigma Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969167" title="Troy-Brown Fuddle Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-33_ef09edd5-d499-489d-8cc2-b88ea82e6417.jpg?v=1733891523" style="width:92px;height:92px"><span>Troy-Brown Fuddle Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969154" title="Troy-Almond Squeeze Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-20_3b13b6e5-2483-40de-93a8-569ada45d06f.jpg?v=1733891553" style="width:92px;height:92px"><span>Troy-Almond Squeeze Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969172" title="Troy-Aqua Teaser Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-38_311a9f83-3d35-41b6-aed1-897583d8babc.jpg?v=1733891513" style="width:92px;height:92px"><span>Troy-Aqua Teaser Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969170" title="Troy-Penny Grabber Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-36_a5b755f9-d78c-4ada-9abf-41ed71ab7221.jpg?v=1733891517" style="width:92px;height:92px"><span>Troy-Penny Grabber Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969157" title="Troy-Polar Puzzler Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-23_e59e025d-724e-43e3-a9b9-f2cd5a3f227a.jpg?v=1733891544" style="width:92px;height:92px"><span>Troy-Polar Puzzler Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969164" title="Troy-Shady Maze Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-30_ae700010-5839-4972-8c72-14d688f7c349.jpg?v=1733891530" style="width:92px;height:92px"><span>Troy-Shady Maze Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969160" title="Troy-Teal Twister Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-26_d8aaeb2b-a588-4c53-962b-f2945dd91342.jpg?v=1733891538" style="width:92px;height:92px"><span>Troy-Teal Twister Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969152" title="Troy-Catch 22 Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-12_9ab268f5-49d3-4b93-8f1c-caad886ef3ea.jpg?v=1733891557" style="width:92px;height:92px"><span>Troy-Catch 22 Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969171" title="Troy-Crimson Thrill Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-37_2f454dba-c845-474f-8629-e6a1fb86740f.jpg?v=1733891515" style="width:92px;height:92px"><span>Troy-Crimson Thrill Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969165" title="Troy-Golden Dilemna Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-CN-31_c0c5b4ab-8506-4120-8265-7ade72c3d148.jpg?v=1733891528" style="width:92px;height:92px"><span>Troy-Golden Dilemna Durable Vinyl / Phillipe Romano</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-23_65500001-a88c-4664-a4b1-96391dbee2a4.jpg?v=1733890123" loading="lazy">
+ <div class="meta">
+ <div class="pname">Merrimack</div>
+ <div class="ven">Phillipe Romano</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 14 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#1c7c3c">● image-confirmed colorways</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/XXP-969731" title="Merrimack-Oak Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-23_65500001-a88c-4664-a4b1-96391dbee2a4.jpg?v=1733890123" style="width:92px;height:92px"><span>Merrimack-Oak Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969721" title="Merrimack-Canvas Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-13_22da4288-d68b-44aa-9f17-341e52162ff7.jpg?v=1733890151" style="width:92px;height:92px"><span>Merrimack-Canvas Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969718" title="Merrimack-Cloud Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-10_44265c33-bdca-41f4-95c3-cc28cb95d5f4.jpg?v=1733890157" style="width:92px;height:92px"><span>Merrimack-Cloud Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969732" title="Merrimack-Carmine Rose Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-24_cf25c7e1-77eb-4286-95c1-6d7c0854aef0.jpg?v=1733890120" style="width:92px;height:92px"><span>Merrimack-Carmine Rose Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969715" title="Merrimack-Cool Grey Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-07_8116c4d1-4d7d-4620-847b-d4991e56ca27.jpg?v=1733890164" style="width:92px;height:92px"><span>Merrimack-Cool Grey Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969713" title="Merrimack-Blank Page Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-05_71d60c69-f7ad-4779-a967-ec03173b16ea.jpg?v=1733890169" style="width:92px;height:92px"><span>Merrimack-Blank Page Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969723" title="Merrimack-Warm Greige Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-15_3cff34bb-c32a-44a1-aef1-eae503d2bd48.jpg?v=1733890146" style="width:92px;height:92px"><span>Merrimack-Warm Greige Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969709" title="Merrimack-Pale Sage Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-01_8c6bf691-c128-4db4-b6b7-e987f440aeca.jpg?v=1733890179" style="width:92px;height:92px"><span>Merrimack-Pale Sage Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969714" title="Merrimack-Grey Felt Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-06_c7bfddd6-bac3-46fa-8637-9b4a63821d09.jpg?v=1733890167" style="width:92px;height:92px"><span>Merrimack-Grey Felt Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969726" title="Merrimack-Beige Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-18_c0d67d78-5388-4c46-ac01-ac83ac02dbb3.jpg?v=1733890136" style="width:92px;height:92px"><span>Merrimack-Beige Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969728" title="Merrimack-Tea Wash Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-20_2feefc87-25ee-43ce-87a5-dc5e0ff4c687.jpg?v=1733890131" style="width:92px;height:92px"><span>Merrimack-Tea Wash Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969712" title="Merrimack-Fountain Pen Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-04_510e01ff-9323-4d36-9438-10adc1602975.jpg?v=1733890172" style="width:92px;height:92px"><span>Merrimack-Fountain Pen Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969710" title="Merrimack-Fresh Fern Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-02_ca7a6bec-2310-4571-af89-facef2e3aeb9.jpg?v=1733890176" style="width:92px;height:92px"><span>Merrimack-Fresh Fern Durable Vinyl / Phillipe Romano</span></a><a class="cw" href="https://designerwallcoverings.com/products/XXP-969724" title="Merrimack-Bark Durable Vinyl / Phillipe Romano"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-TX-16_f2a4cc21-d1e3-4e12-9569-08bd9d337e53.jpg?v=1733890144" style="width:92px;height:92px"><span>Merrimack-Bark Durable Vinyl / Phillipe Romano</span></a></div>
+ </div>
+ <div class="tile">
+ <img class="hero" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWYA-60015-DesignerWallcoverings-Los-AngelesMaya-Romanoff-Wallpaper-MR-NE-1851Rustic-NordicOak-1.jpg?v=1738861056" loading="lazy">
+ <div class="meta">
+ <div class="pname">Elements</div>
+ <div class="ven">Maya Romanoff</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots"><i></i><i></i><i></i></span>
+ 4 Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:#9a2b2b">● 4 confirmed · 10 split off as different patterns</div>
+ </div>
+ <div class="ways"><a class="cw" href="https://designerwallcoverings.com/products/DWYA-60015" title="Natural Elements - Rustic - Nordic Oak / Maya Romanoff"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWYA-60015-DesignerWallcoverings-Los-AngelesMaya-Romanoff-Wallpaper-MR-NE-1851Rustic-NordicOak-1.jpg?v=1738861056" style="width:92px;height:92px"><span>Natural Elements - Rustic - Nordic Oak / Maya Romanoff</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWYA-60012" title="Natural Elements - Refined - Carbonized Silver Birch / Maya Romanoff"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWYA-60012-DesignerWallcoverings-Los-AngelesMaya-Romanoff-Wallpaper-MR-NE-1222Refined-CarbonizedSilverBirch-1.jpg?v=1738861060" style="width:92px;height:92px"><span>Natural Elements - Refined - Carbonized Silver Birch / Maya Romanoff</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWYA-60014" title="Natural Elements - Refined - Mahogany / Maya Romanoff"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWYA-60014-DesignerWallcoverings-Los-AngelesMaya-Romanoff-Wallpaper-MR-NE-1608Refined-Mahogany-1.jpg?v=1738861057" style="width:92px;height:92px"><span>Natural Elements - Refined - Mahogany / Maya Romanoff</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWYA-60020" title="Natural Elements - Refined - Ash / Maya Romanoff"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWYA-60020-DesignerWallcoverings-Los-AngelesMaya-Romanoff-Wallpaper-MR-NE-1X02Refined-Ash-1.jpg?v=1738861048" style="width:92px;height:92px"><span>Natural Elements - Refined - Ash / Maya Romanoff</span></a></div>
+ </div></div>
+ <h2>Why it's safe — over-grouping is caught, not merged</h2>
+ <p>Candidate key grouped <b>14</b> Coordonné products under one generic title. The image check finds <b>12</b> genuinely different patterns — so they are <b>kept separate</b>, not collapsed into a false "14 Colors" tile:</p><div class="diag"><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330019" title="Coordonné Interlock Wallcovering / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/6461186_0936a242-86ec-43b8-a53b-17f3dfe18f8f.jpg?v=1739303768" style="width:70px;height:70px"><span>Coordonné Interlock Wallcovering / Coordone Europe</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330787" title="Clover Cacao / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00317_01_51ea4e7e-1e9f-4955-b37b-dfeb0fcac7d9.jpg?v=1739302344" style="width:70px;height:70px"><span>Clover Cacao / Coordone Europe</span></a><small>pattern 1</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330796" title="Paisley Blue / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00326_01_6a2e1d88-df43-4253-bf51-50abd8c301bc.jpg?v=1739302325" style="width:70px;height:70px"><span>Paisley Blue / Coordone Europe</span></a><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330965" title="Forja Avio / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/YSP0409_01.jpg?v=1739301948" style="width:70px;height:70px"><span>Forja Avio / Coordone Europe</span></a><small>pattern 2</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330617" title="Sergé Taupe III / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00021_01_2.jpg?v=1739302665" style="width:70px;height:70px"><span>Sergé Taupe III / Coordone Europe</span></a><small>pattern 3</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330716" title="Crowned Crane Rose / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00163_01_1d272c72-76b0-4aa7-9b7a-4dd6ee673042.jpg?v=1739302476" style="width:70px;height:70px"><span>Crowned Crane Rose / Coordone Europe</span></a><small>pattern 4</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/COR-23079" title="Open Basket Weave / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/products/2079cb76d2ace382a572a94b950f1779.jpg?v=1739304005" style="width:70px;height:70px"><span>Open Basket Weave / Coordone Europe</span></a><small>pattern 5</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330475" title="Leku Estaño / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/A00648_01.jpg?v=1739302934" style="width:70px;height:70px"><span>Leku Estaño / Coordone Europe</span></a><small>pattern 6</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330647" title="Raphia Glacier B00052 / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00052_01_2_2b37a09f-a9fd-413a-990a-dc28c290837a.jpg?v=1739302610" style="width:70px;height:70px"><span>Raphia Glacier B00052 / Coordone Europe</span></a><small>pattern 7</small></div><div class="dcl"><a class="cw" href="https://designerwallcoverings.com/products/DWDC-330456" title="Pradera Rosa / Coordone Europe"><img loading="lazy" src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/A00629_01.jpg?v=1739302964" style="width:70px;height:70px"><span>Pradera Rosa / Coordone Europe</span></a><small>pattern 8</small></div></div>
+</div>
\ No newline at end of file
diff --git a/tools/pattern-grouping-demo.py b/tools/pattern-grouping-demo.py
new file mode 100644
index 0000000..e832d62
--- /dev/null
+++ b/tools/pattern-grouping-demo.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+"""Visual proof of the collapse-colorways feature (READ-ONLY, local, $0).
+
+For a handful of real DW colorway families it:
+ 1. derives a candidate pattern (strip vendor suffix + product-type noise + color words),
+ 2. CONFIRMS with the color-invariant image hash which members are truly the same
+ pattern in different colors (the 'for more confirmation' step),
+ 3. renders an HTML page showing the AFTER collection grid — one tile per pattern with
+ an 'N Colors' chip that expands to the confirmed colorways — plus a diagnostics
+ panel proving an over-grouped key (Coordonné) is correctly NOT merged.
+
+Reads "dw_sku|title|vendor|image_url" lines on stdin. Writes tools/grouping-demo.html.
+"""
+import sys, io, re, os, urllib.request, hashlib, html, json
+from PIL import Image, ImageOps
+
+THRESH = int(os.environ.get("HAM_THRESH", "16"))
+HASH = 8
+CACHE = "/tmp/pattern-verify-cache"
+os.makedirs(CACHE, exist_ok=True)
+
+COLOR_WORDS = set("""beige black blue brown copper cream gold gray grey green ivory navy orange
+peach pearl pink purple red rose rust sage salmon silver tan taupe teal turquoise white yellow
+mauve plum aqua charcoal khaki lavender olive platinum bronze mustard indigo burgundy coral mint
+seafoam sand stone snow mushroom natural oatmeal flax flaxen cedar flint winter chrome graphite
+witchgrass hay lemongrass cotton bluegrass barley meadow caramel sunflower toast pearl smoke ash
+light dark deep pale soft warm cool muted bright antique metallic neutral multi""".split())
+NOISE = ["durable vinyl","wallcovering","wallcoverings","wallpaper","fabric","fabrics","vinyl",
+ "grasscloth","print","commercial","residential","mural","self adhesive","with surface stick",
+ "by phillipe romano","phillipe romano"]
+
+def base_of(title):
+ t = title.split("|")[0].strip().lower()
+ for n in NOISE: t = t.replace(n, " ")
+ # split colorway after a dash if present: "albany linen-snow" -> "albany linen"
+ t = re.split(r"[-–]", t)[0] if re.search(r"[-–]", t) else t
+ words = [w for w in re.sub(r"[^a-z0-9 ]", " ", t).split() if w and w not in COLOR_WORDS]
+ return " ".join(words).strip()
+
+def dhash(img, n=HASH):
+ g = img.convert("L").resize((n + 1, n), Image.LANCZOS)
+ px = list(g.getdata()); v = 0
+ for r in range(n):
+ b = r * (n + 1)
+ for c in range(n):
+ v = (v << 1) | (1 if px[b + c] > px[b + c + 1] else 0)
+ return v
+
+def fetch(url):
+ key = os.path.join(CACHE, hashlib.md5(url.encode()).hexdigest() + ".bin")
+ if os.path.exists(key):
+ return open(key, "rb").read()
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 dw"})
+ d = urllib.request.urlopen(req, timeout=20).read()
+ open(key, "wb").write(d); return d
+
+def sig(url):
+ im = Image.open(io.BytesIO(fetch(url)))
+ return dhash(im), dhash(ImageOps.invert(im.convert("L")))
+
+def ham(a, b): return bin(a ^ b).count("1")
+
+# ---- ingest ----
+rows = []
+for line in sys.stdin:
+ p = line.rstrip("\n").split("|", 3)
+ if len(p) == 4 and p[3].startswith("http"):
+ rows.append({"sku": p[0], "title": p[1], "vendor": p[2], "url": p[3]})
+
+# group by (vendor, base)
+fam = {}
+for r in rows:
+ b = base_of(r["title"])
+ if not b: continue
+ fam.setdefault((r["vendor"], b), []).append(r)
+
+# keep families with >=4 candidates; cap images per family
+families = [(k, v[:14]) for k, v in fam.items() if len(v) >= 4]
+families.sort(key=lambda kv: len(kv[1]), reverse=True)
+families = families[:10]
+
+def confirm(members):
+ """Return list of clusters (each a list of member dicts) by image structure."""
+ sigs = []
+ for m in members:
+ try:
+ h, hi = sig(m["url"]); sigs.append((m, h, hi))
+ except Exception:
+ pass
+ clusters = []
+ for m, h, hi in sigs:
+ for cl in clusters:
+ if any(min(ham(h, ch), ham(hi, ch)) <= THRESH for (_, ch, _) in cl):
+ cl.append((m, h, hi)); break
+ else:
+ clusters.append([(m, h, hi)])
+ clusters.sort(key=len, reverse=True)
+ return clusters
+
+tiles = []
+for (vendor, base), members in families:
+ clusters = confirm(members)
+ if not clusters: continue
+ dom = clusters[0]
+ confirmed = [m for (m, _, _) in dom]
+ pct = len(dom) / sum(len(c) for c in clusters)
+ tiles.append({
+ "pattern": base.title(), "vendor": vendor,
+ "confirmed": confirmed, "n_clusters": len(clusters),
+ "split_off": sum(len(c) for c in clusters[1:]),
+ "status": "confirmed" if (len(clusters) == 1) else ("mostly" if pct >= 0.6 else "mixed"),
+ })
+
+# ---- render ----
+def esc(s): return html.escape(str(s or ""))
+def card(m, size=120):
+ return (f'<a class="cw" href="https://designerwallcoverings.com/products/{esc(m["sku"])}" '
+ f'title="{esc(m["title"])}"><img loading="lazy" src="{esc(m["url"])}" '
+ f'style="width:{size}px;height:{size}px"><span>{esc(m["title"].split("|")[0])}</span></a>')
+
+tile_html = ""
+for t in tiles:
+ n = len(t["confirmed"])
+ rep = t["confirmed"][0]
+ badge = {"confirmed": "#1c7c3c", "mostly": "#8a6d00", "mixed": "#9a2b2b"}[t["status"]]
+ note = ("image-confirmed colorways" if t["status"] == "confirmed"
+ else f"{n} confirmed · {t['split_off']} split off as different patterns")
+ swatches = "".join(card(m, 92) for m in t["confirmed"])
+ tile_html += f'''
+ <div class="tile">
+ <img class="hero" src="{esc(rep["url"])}" loading="lazy">
+ <div class="meta">
+ <div class="pname">{esc(t["pattern"])}</div>
+ <div class="ven">{esc(t["vendor"])}</div>
+ <button class="chip" onclick="this.closest('.tile').classList.toggle('open')">
+ <span class="dots">{"".join('<i></i>' for _ in range(min(n,3)))}</span>
+ {n} Colors <span class="car">⌄</span>
+ </button>
+ <div class="note" style="color:{badge}">● {esc(note)}</div>
+ </div>
+ <div class="ways">{swatches}</div>
+ </div>'''
+
+# diagnostics: Coordonné over-group correctly NOT merged
+diag = ""
+coord = [r for r in rows if r["vendor"] == "Coordonné"][:14]
+if coord:
+ cl = confirm(coord)
+ diag = (f'<p>Candidate key grouped <b>{len(coord)}</b> Coordonné products under one generic title. '
+ f'The image check finds <b>{len(cl)}</b> genuinely different patterns — so they are '
+ f'<b>kept separate</b>, not collapsed into a false "{len(coord)} Colors" tile:</p>'
+ '<div class="diag">' +
+ "".join('<div class="dcl">' + "".join(card(m, 70) for (m, _, _) in c[:6]) +
+ f'<small>pattern {i+1}</small></div>' for i, c in enumerate(cl[:8])) +
+ '</div>')
+
+out = f'''<!doctype html><meta charset=utf-8>
+<title>DW · Collapse colorways — visual proof</title>
+<style>
+ body{{font:15px/1.5 -apple-system,Segoe UI,sans-serif;margin:0;background:#faf8f5;color:#1a1a1a}}
+ header{{padding:22px 28px;background:#111;color:#fff}}
+ header h1{{margin:0;font-size:19px;font-weight:600;letter-spacing:.02em}}
+ header p{{margin:6px 0 0;color:#bbb;font-size:13px}}
+ .wrap{{padding:24px 28px;max-width:1280px;margin:0 auto}}
+ h2{{font:600 15px/1 sans-serif;text-transform:uppercase;letter-spacing:.08em;color:#666;margin:34px 0 14px}}
+ .grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:18px}}
+ .tile{{background:#fff;border:1px solid #e7e2da;border-radius:8px;overflow:hidden}}
+ .tile .hero{{width:100%;aspect-ratio:1;object-fit:cover;display:block}}
+ .meta{{padding:11px 13px}}
+ .pname{{font-weight:600;font-size:14px}}
+ .ven{{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:#888;margin:2px 0 9px}}
+ .chip{{display:inline-flex;align-items:center;gap:7px;border:1px solid #d8d2c8;background:#fff;
+ border-radius:999px;padding:5px 12px;font-size:13px;font-weight:600;cursor:pointer}}
+ .chip .dots{{display:inline-flex}}
+ .chip .dots i{{width:15px;height:15px;border-radius:50%;background:#cabfa9;border:2px solid #fff;margin-right:-6px;box-shadow:0 0 0 1px #d8d2c8}}
+ .chip .car{{transition:transform .2s}}
+ .tile.open .chip .car{{transform:rotate(180deg)}}
+ .note{{font-size:11px;margin-top:8px}}
+ .ways{{display:none;flex-wrap:wrap;gap:8px;padding:4px 13px 14px}}
+ .tile.open .ways{{display:flex}}
+ .cw{{display:block;text-decoration:none;color:#444;text-align:center}}
+ .cw img{{object-fit:cover;border-radius:5px;border:1px solid #e7e2da;display:block}}
+ .cw span{{display:block;font-size:9px;max-width:92px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:3px;color:#888}}
+ .diag{{display:flex;flex-wrap:wrap;gap:14px}}
+ .dcl{{border:1px dashed #c9b; border-radius:8px;padding:8px;background:#fff}}
+ .dcl small{{display:block;text-align:center;color:#9a2b9a;font-size:10px;margin-top:4px;text-transform:uppercase}}
+ .legend{{font-size:12px;color:#555;background:#fff;border:1px solid #e7e2da;border-radius:8px;padding:12px 16px}}
+</style>
+<header>
+ <h1>Designer Wallcoverings — collapse colorways into one pattern tile</h1>
+ <p>Real catalog data · each tile = one pattern · “N Colors” expands to the colorways · grouping confirmed by the color-invariant image check (dHash≤{THRESH}, $0 local)</p>
+</header>
+<div class="wrap">
+ <div class="legend">Click a <b>“N Colors ⌄”</b> chip to expand the confirmed colorways. The coloured dot under each tile reports the image-confirmation status:
+ <span style="color:#1c7c3c">● confirmed</span> · <span style="color:#8a6d00">● mostly (outliers split off)</span> · <span style="color:#9a2b2b">● mixed</span>.</div>
+ <h2>Collection grid — after (one tile per pattern)</h2>
+ <div class="grid">{tile_html}</div>
+ <h2>Why it's safe — over-grouping is caught, not merged</h2>
+ {diag or "<p>(no Coordonné sample in input)</p>"}
+</div>'''
+
+path = os.path.join(os.path.dirname(__file__), "grouping-demo.html")
+open(path, "w").write(out)
+print("wrote", path, "·", len(tiles), "pattern tiles")
diff --git a/tools/pattern-id-backfill-report.json b/tools/pattern-id-backfill-report.json
new file mode 100644
index 0000000..0b2ab85
--- /dev/null
+++ b/tools/pattern-id-backfill-report.json
@@ -0,0 +1,417 @@
+{
+ "generated_at": "2026-06-26T06:58:04.281Z",
+ "mode": "DRY-RUN (read-only; no Shopify call, no metafield write)",
+ "active_products": 74927,
+ "mfr_map_entries": 0,
+ "used_mirror_mfr_column": true,
+ "counts": {
+ "distinct_pattern_ids": 63028,
+ "multi_colorway_groups": 3752,
+ "products_in_multi_groups": 15651,
+ "grid_rows_collapsed": 11899,
+ "pct_catalog_collapsed": "15.9%",
+ "avg_group_size": "1.19",
+ "huge_groups_gt60": 4
+ },
+ "key_source_breakdown": {
+ "via_mfr_root": 69906,
+ "via_t2_fallback": 5021,
+ "via_pattern_name_last_resort": 0,
+ "texture_flagged": 19265,
+ "no_image": 0
+ },
+ "texture_aware": {
+ "texture_flagged_products": 19265,
+ "texture_multi_colorway_groups": 1194,
+ "skip_gate_groups_mfr_root": 1136,
+ "note": "texture exception relaxes the image gate ONLY for mfr_root-sourced groups (trust the root). texture T2 over-merges are routed to the local-vision bucket, NOT auto-trusted."
+ },
+ "image_confirm": {
+ "dhash_groups": 2558,
+ "dhash_products": 8435,
+ "local_vision_groups": 58,
+ "local_vision_products": 511,
+ "note": "dHash bucket = non-texture multi-groups (pattern-image-verify.py, $0 local). local_vision bucket = texture multi-groups w/o a trusted mfr root (Ollama qwen2.5vl, $0). NEITHER run in dry-run."
+ },
+ "top_groups": [
+ {
+ "n": 178,
+ "vendor": "Versace",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "versace::mfr::SANCAR",
+ "eg": [
+ "Versace Wallcovering",
+ "Regalito Bay | Versace Wallcovering",
+ "Versace Wallcovering",
+ "Versace Wallcovering"
+ ]
+ },
+ {
+ "n": 93,
+ "vendor": "Ultrasuede",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "ultrasuede::mfr::5538",
+ "eg": [
+ "Ultrasuede™ Chamois Cream | Wallcovering & Fabric",
+ "Ultrasuede™ Fawn Brown | Wallcovering & Fabric",
+ "Ultrasuede™ Pewter Grey | Wallcovering & Fabric",
+ "Ultrasuede™ Tea Rose Pink | Wallcovering & Fabric"
+ ]
+ },
+ {
+ "n": 81,
+ "vendor": "Innovations USA",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "innovations-usa::mfr::TRUE",
+ "eg": [
+ "Spalt | Innovations USA",
+ "Origami | Innovations USA",
+ "Sumatra | Innovations USA",
+ "Mazarin | Innovations USA"
+ ]
+ },
+ {
+ "n": 64,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": false,
+ "pattern_id": "hollywood-wallcoverings::mfr::DY24",
+ "eg": [
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 55,
+ "vendor": "British Walls",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "british-walls::mfr::OSBORNE",
+ "eg": [
+ "MIllionaire Marble Wallcovering",
+ "Palm Jungle Life Wallcovering",
+ "Exotica Flamingo | British Walls",
+ "Palm Jungle Life Wallcovering"
+ ]
+ },
+ {
+ "n": 52,
+ "vendor": "Phillipe Romano",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "phillipe-romano::mfr::INNOVATIONS",
+ "eg": [
+ "Cretano | Phillipe Romano",
+ "Cretano | Phillipe Romano",
+ "Labarina | Phillipe Romano",
+ "Amalina | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 45,
+ "vendor": "Phillipe Romano",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "phillipe-romano::mfr::488",
+ "eg": [
+ "Decorator Grasscloth Vol. 2 | Phillipe Romano",
+ "Decorator Grasscloth Vol. 2 | Phillipe Romano",
+ "Decorator Grasscloth Vol. 2 | Phillipe Romano",
+ "Decorator Grasscloth Vol. 2 | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 44,
+ "vendor": "Koroseal",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "koroseal::mfr::C521",
+ "eg": [
+ "Chimayo O'Keefe White - Beige Commercial Wallcovering | Koroseal",
+ "Chimayo Tierra Bayita - Beige Commercial Wallcovering | Koroseal",
+ "Chimayo Canyon Shadow - Beige Commercial Wallcovering | Koroseal",
+ "Chimayo Santa Fe - Beige Commercial Wallcovering | Koroseal"
+ ]
+ },
+ {
+ "n": 41,
+ "vendor": "Versace",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "versace::mfr::YORK",
+ "eg": [
+ "Narcicus | Versace",
+ "Narcicus | Versace",
+ "Regatania Island | Versace",
+ "Regatania Island | Versace"
+ ]
+ },
+ {
+ "n": 37,
+ "vendor": "Brand McKenzie",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "brand-mckenzie::mfr::BMPP004",
+ "eg": [
+ "Lido Turquoise Wallcovering | Brand McKenzie",
+ "Mixology Rainbow Wallcovering | Brand McKenzie",
+ "Charming Lanes Denim Wallcovering | Brand McKenzie",
+ "The Mediterranean Stone Wallcovering | Brand McKenzie"
+ ]
+ },
+ {
+ "n": 36,
+ "vendor": "Brand McKenzie",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "brand-mckenzie::mfr::BMTD001",
+ "eg": [
+ "The Green House Midnight Blue | Brand McKenzie",
+ "Tropical Forest Dark Green & Yellow - Sage Green Wallcovering | Brand McKenzie",
+ "Alpine Landscape Alpine Green - Jade Green Wallcovering | Brand McKenzie",
+ "Pampas Grass Cornflower Blue - Light Gray Wallcovering | Brand McKenzie"
+ ]
+ },
+ {
+ "n": 35,
+ "vendor": "Brand McKenzie",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "brand-mckenzie::mfr::BMHD002",
+ "eg": [
+ "Feather Palm Rose - Pale Pink Wallcovering | Brand McKenzie",
+ "Feather Palm Sky Blue | Brand McKenzie",
+ "Downtown Deco Pastel Blue Wallcovering | Brand McKenzie",
+ "The Great Damask Dusky Pink - Beige Wallcovering | Brand McKenzie"
+ ]
+ },
+ {
+ "n": 35,
+ "vendor": "Brand McKenzie",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "brand-mckenzie::mfr::BMCF003",
+ "eg": [
+ "Starry Clouds Green Sky - Light Blue Wallcovering | Brand McKenzie",
+ "Carousel Pink Wallcovering | Brand McKenzie",
+ "Starry Clouds Nightfall Wallcovering | Brand McKenzie",
+ "Fiesta Ruby & Green Wallcovering | Brand McKenzie"
+ ]
+ },
+ {
+ "n": 34,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::PANAMA-LINEN-A121",
+ "eg": [
+ "Eatonville Faux Linen Durable | Hollywood Wallcoverings",
+ "Eatonville Faux Linen Durable | Hollywood Wallcoverings",
+ "Paddock Type II Vinyl | Hollywood Wallcoverings",
+ "Paddock Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 34,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::SOVELLE-L2-NV",
+ "eg": [
+ "Shubert Faux Rice Paper Durable | Hollywood Wallcoverings",
+ "Shubert Faux Rice Paper Durable | Hollywood Wallcoverings",
+ "Shubert Faux Rice Paper Durable | Hollywood Wallcoverings",
+ "Shubert Faux Rice Paper Durable | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 34,
+ "vendor": "Koroseal",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "koroseal::mfr::LN21",
+ "eg": [
+ "Lino Lively Yellow - Mustard Yellow Commercial Wallcovering | Koroseal",
+ "Lino Socialite Brown - Burnt Sienna Commercial Wallcovering | Koroseal",
+ "Lino Hang Dry Gray - Light Gray Commercial Wallcovering | Koroseal",
+ "Lino Artisan Black - Charcoal Fabric | Koroseal"
+ ]
+ },
+ {
+ "n": 32,
+ "vendor": "Newmor Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "newmor-wallcoverings::mfr::BST",
+ "eg": [
+ "Brushed Steel Commercial Wallcovering",
+ "Brushed Steel Commercial Wallcovering",
+ "Brushed Steel Commercial Wallcovering",
+ "Brushed Steel Commercial Wallcovering"
+ ]
+ },
+ {
+ "n": 29,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::5021",
+ "eg": [
+ "Peter's Plastered | Hollywood Wallcoverings",
+ "Peter's Plastered | Hollywood Wallcoverings",
+ "Peter's Plastered | Hollywood Wallcoverings",
+ "Peter's Plastered | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 29,
+ "vendor": "Newmor Wallcoverings",
+ "source": "mfr_root",
+ "texture": false,
+ "pattern_id": "newmor-wallcoverings::mfr::DEL",
+ "eg": [
+ "Delano Commercial Wallcovering",
+ "Delano Commercial Wallcovering",
+ "Delano Commercial Wallcovering",
+ "Delano Commercial Wallcovering"
+ ]
+ },
+ {
+ "n": 28,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::SILENCE-ASIL",
+ "eg": [
+ "Sidmouth Acoustical | Hollywood Wallcoverings",
+ "Sidmouth Acoustical | Hollywood Wallcoverings",
+ "Sidmouth Acoustical | Hollywood Wallcoverings",
+ "Sidmouth Acoustical | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 27,
+ "vendor": "Versa Designed Surfaces",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "versa-designed-surfaces::mfr::A122",
+ "eg": [
+ "Bellagio - Shoji Wallcovering | Versa Designed Surfaces",
+ "Bellagio - Soleil Wallcovering | Versa Designed Surfaces",
+ "Bellagio - Crown Jewel Wallcovering | Versa Designed Surfaces",
+ "Bellagio - Diamond White Wallcovering | Versa Designed Surfaces"
+ ]
+ },
+ {
+ "n": 27,
+ "vendor": "Newmor Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "newmor-wallcoverings::mfr::TEL",
+ "eg": [
+ "Tela II Commercial Wallcovering",
+ "Tela II Commercial Wallcovering",
+ "Tela II Commercial Wallcovering",
+ "Tela II Commercial Wallcovering"
+ ]
+ },
+ {
+ "n": 27,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::BACCARAT-R2-BC",
+ "eg": [
+ "Vernon Durable | Hollywood Wallcoverings",
+ "Vernon Durable | Hollywood Wallcoverings",
+ "Vernon Durable | Hollywood Wallcoverings",
+ "Oxford Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 26,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "hollywood-wallcoverings::mfr::SURF-L2-SF",
+ "eg": [
+ "Hollywood Contemporary Coast | Hollywood Wallcoverings",
+ "Hollywood Contemporary Coast | Hollywood Wallcoverings",
+ "Hollywood Contemporary Coast | Hollywood Wallcoverings",
+ "Hollywood Contemporary Coast | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 26,
+ "vendor": "Koroseal",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "koroseal::mfr::5021",
+ "eg": [
+ "Grande Aloe - Pale Green Commercial Wallcovering | Koroseal",
+ "Grande Tumbleweed - Light Tan Commercial Wallcovering | Koroseal",
+ "Grande Golden Harvest - Beige Commercial Wallcovering | Koroseal",
+ "Grande Cold Creek - Steel Blue Commercial Wallcovering | Koroseal"
+ ]
+ }
+ ],
+ "huge_groups_for_review": [
+ {
+ "n": 178,
+ "vendor": "Versace",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "versace::mfr::SANCAR",
+ "eg": [
+ "Versace Wallcovering",
+ "Regalito Bay | Versace Wallcovering",
+ "Versace Wallcovering",
+ "Versace Wallcovering"
+ ]
+ },
+ {
+ "n": 93,
+ "vendor": "Ultrasuede",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "ultrasuede::mfr::5538",
+ "eg": [
+ "Ultrasuede™ Chamois Cream | Wallcovering & Fabric",
+ "Ultrasuede™ Fawn Brown | Wallcovering & Fabric",
+ "Ultrasuede™ Pewter Grey | Wallcovering & Fabric",
+ "Ultrasuede™ Tea Rose Pink | Wallcovering & Fabric"
+ ]
+ },
+ {
+ "n": 81,
+ "vendor": "Innovations USA",
+ "source": "mfr_root",
+ "texture": true,
+ "pattern_id": "innovations-usa::mfr::TRUE",
+ "eg": [
+ "Spalt | Innovations USA",
+ "Origami | Innovations USA",
+ "Sumatra | Innovations USA",
+ "Mazarin | Innovations USA"
+ ]
+ },
+ {
+ "n": 64,
+ "vendor": "Hollywood Wallcoverings",
+ "source": "mfr_root",
+ "texture": false,
+ "pattern_id": "hollywood-wallcoverings::mfr::DY24",
+ "eg": [
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tools/pattern-id-backfill.js b/tools/pattern-id-backfill.js
new file mode 100644
index 0000000..697c9a9
--- /dev/null
+++ b/tools/pattern-id-backfill.js
@@ -0,0 +1,308 @@
+#!/usr/bin/env node
+/**
+ * pattern-id-backfill.js — Option-A (Hybrid + image-confirm) pattern_id computer.
+ *
+ * Computes the final `custom.pattern_id` per ACTIVE product per the DTD-committed
+ * Option-A pipeline:
+ * 1. mfr_sku ROOT (series-aware) is the PRIMARY grouping key when available.
+ * The authoritative mfr_sku source is the Shopify metafield
+ * `custom.manufacturer_sku`, which is NOT in the local mirror — so this
+ * script accepts a mfr_sku MAP as an INPUT FILE (produced by the GATED
+ * Shopify Admin pull). Default = empty map → proves the T2 fallback path.
+ * 2. A color-invariant IMAGE-CONFIRM hash is the GUARD that splits over-grouped
+ * keys. In DRY-RUN we do NOT fetch 74k images; we FLAG which products/groups
+ * would need the image-confirm pass (non-texture multi-colorway members).
+ * 3. TEXTURE-AWARE EXCEPTION: products tagged with organic textures
+ * (grasscloth/linen/raffia/cork/…) TRUST the mfr_sku root and SKIP the image
+ * gate (the hash false-splits random fibers). These are flagged texture=true.
+ * 4. T2 normalized-title (strip vendor suffix + product-type noise + color words)
+ * is the FALLBACK key ONLY where no mfr_sku root exists.
+ *
+ * DRY-RUN ONLY. Reads the read-only mirror + an optional mfr map; WRITES a report
+ * (json + console). It NEVER calls the Shopify API and NEVER writes a metafield.
+ *
+ * Usage:
+ * node tools/pattern-id-backfill.js # empty map → pure T2 fallback
+ * node tools/pattern-id-backfill.js --mfr-map FILE.json # {key: mfrSku} input from gated pull
+ * node tools/pattern-id-backfill.js --use-mirror-mfr # read-only: use mirror.mfr_sku column as interim source
+ * node tools/pattern-id-backfill.js --apply # GATED NO-OP stub: prints + exits, writes nothing
+ *
+ * mfr-map FILE format (either keying works; dw_sku preferred, shopify_id accepted):
+ * { "DWWC12345": "TAK-AA04-01", "gid://shopify/Product/123": "FRL5226/01", ... }
+ */
+require('dotenv').config();
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+const { COLORS } = require('../lib/classify');
+
+const ARGV = process.argv.slice(2);
+const APPLY = ARGV.includes('--apply');
+const USE_MIRROR_MFR = ARGV.includes('--use-mirror-mfr');
+const mfrMapIdx = ARGV.indexOf('--mfr-map');
+const MFR_MAP_FILE = mfrMapIdx !== -1 ? ARGV[mfrMapIdx + 1] : null;
+
+// ---- HARD GATE: --apply is a no-op stub. The 74k metafieldsSet WRITE is Steve-gated. ----
+if (APPLY) {
+ console.log('────────────────────────────────────────────────────────');
+ console.log(' --apply is GATED — requires Steve sign-off.');
+ console.log(' Writing custom.pattern_id to ~74k Shopify products is a');
+ console.log(' bulk customer-facing catalog change + Kamatera-canonical');
+ console.log(' dw_unified write. This stub writes NOTHING and exits.');
+ console.log(' See the pending-approval memo for the gated command.');
+ console.log('────────────────────────────────────────────────────────');
+ process.exit(0);
+}
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+
+// ---------- text normalization (shared shape with pattern-grouping-dryrun.js) ----------
+const NOISE = [
+ 'wallcovering','wallcoverings','wallpaper','wall covering','fabric','fabrics',
+ 'durable vinyl','vinyl','grasscloth','print','commercial','residential',
+ 'type ii','type iii','mural','wall mural','peel and stick','peel-and-stick',
+ 'self adhesive','self-adhesive','by phillipe romano','phillipe romano'
+];
+const COLOR_WORDS = new Set([...COLORS,
+ 'light','dark','deep','pale','soft','warm','cool','muted','bright','antique',
+ 'metallic','natural','neutral','multi','multicolor','multicolour'
+]);
+
+// organic-fiber materials whose RANDOM fiber structure false-splits the image hash.
+// DTD named grasscloth/linen/raffia/cork; close organic-fiber cousins included.
+// (silk/suede/flocked deliberately EXCLUDED — those tile uniformly, hash is reliable.)
+const TEXTURE_MATERIALS = [
+ 'grasscloth','grass cloth','linen','raffia','cork','sisal','jute','abaca',
+ 'hemp','seagrass','sea grass','paperweave','paper weave','bamboo','natural fiber'
+];
+
+function stripVendorSuffix(title) {
+ const i = (title || '').indexOf('|');
+ return (i === -1 ? (title || '') : title.slice(0, i)).trim();
+}
+function normTitle(title) {
+ let s = stripVendorSuffix(title).toLowerCase();
+ for (const n of NOISE) s = s.split(n).join(' ');
+ s = s.replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
+ return s;
+}
+function stripColors(normalized) {
+ const kept = normalized.split(' ').filter(w => w && !COLOR_WORDS.has(w));
+ return kept.join(' ').trim() || normalized;
+}
+
+// ---------- series-aware mfr_sku ROOT ----------
+// Drop a TRAILING colorway suffix while preserving the pattern/series root:
+// TAK-AA04-01 -> TAK-AA04 (drop final numeric token, keep alphanumeric series)
+// FRL5226/01 -> FRL5226
+// W7008-01 -> W7008
+// AT9604 -> AT9604 (no colorway → unchanged)
+// 5004561 -> 5004561 (single pure-numeric token → keep, never empty out)
+function mfrRoot(raw) {
+ if (!raw) return null;
+ let s = String(raw).trim().toUpperCase();
+ if (!s) return null;
+ // split on the common SKU separators
+ const tokens = s.split(/[-/_.\s]+/).filter(Boolean);
+ if (tokens.length >= 2) {
+ const last = tokens[tokens.length - 1];
+ // colorway suffix = 1-3 digits, optionally one trailing letter (01, 001, 01A)
+ if (/^\d{1,3}[A-Z]?$/.test(last)) {
+ return tokens.slice(0, -1).join('-');
+ }
+ }
+ // no separable colorway → root is the whole thing (separators normalized to '-')
+ return tokens.join('-');
+}
+
+function isTexture(tagsLower, titleLower) {
+ const hay = (tagsLower || '') + ' ' + (titleLower || '');
+ return TEXTURE_MATERIALS.some(m => hay.includes(m));
+}
+
+function vendorSlug(v) {
+ return (v || '∅').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'no-vendor';
+}
+
+// ---------- load mfr map (from gated Shopify pull) ----------
+function loadMfrMap() {
+ if (!MFR_MAP_FILE) return {};
+ if (!fs.existsSync(MFR_MAP_FILE)) {
+ console.error(`mfr-map file not found: ${MFR_MAP_FILE} — proceeding with EMPTY map (pure fallback).`);
+ return {};
+ }
+ try {
+ const obj = JSON.parse(fs.readFileSync(MFR_MAP_FILE, 'utf8'));
+ // accept {key: mfr} or {key: {manufacturer_sku: mfr}}
+ const out = {};
+ for (const [k, v] of Object.entries(obj)) {
+ out[k] = typeof v === 'string' ? v : (v && (v.manufacturer_sku || v.mfr_sku || v.value)) || null;
+ }
+ return out;
+ } catch (e) {
+ console.error('Failed to parse mfr-map; using EMPTY map:', e.message);
+ return {};
+ }
+}
+
+(async () => {
+ const mfrMap = loadMfrMap();
+ const mfrMapSize = Object.keys(mfrMap).length;
+ console.log('Loading ACTIVE products from mirror (read-only)…');
+ const r = await pool.query(`
+ SELECT dw_sku, shopify_id, title, vendor, pattern_name, tags, image_url
+ ${USE_MIRROR_MFR ? ', mfr_sku' : ''}
+ FROM shopify_products
+ WHERE status = 'ACTIVE' AND title IS NOT NULL AND handle IS NOT NULL
+ `);
+ const rows = r.rows;
+ console.log('Active products:', rows.length);
+ console.log('mfr-map entries:', mfrMapSize, USE_MIRROR_MFR ? '(+ mirror.mfr_sku column as interim source)' : '');
+
+ const groups = new Map(); // pattern_id -> {pattern_id, vendor, source, key, members:[], texture, samples}
+ const stats = {
+ via_mfr_root: 0, via_t2_fallback: 0, via_pattern_name_last_resort: 0,
+ texture_flagged: 0, no_image: 0
+ };
+
+ for (const row of rows) {
+ const titleLower = (row.title || '').toLowerCase();
+ const tagsLower = (row.tags || '').toLowerCase();
+ const vslug = vendorSlug(row.vendor);
+ const texture = isTexture(tagsLower, titleLower);
+
+ // resolve authoritative mfr sku: input map (preferred) → mirror column (if flag) → none
+ let mfr = mfrMap[row.dw_sku] || mfrMap[row.shopify_id] || null;
+ if (!mfr && USE_MIRROR_MFR) mfr = row.mfr_sku || null;
+
+ let source, key, patternId;
+ const root = mfrRoot(mfr);
+ if (root) {
+ source = 'mfr_root'; key = root;
+ patternId = `${vslug}::mfr::${root}`;
+ stats.via_mfr_root++;
+ } else {
+ // T2 fallback: normalized title minus color words
+ const t2 = stripColors(normTitle(row.title));
+ if (t2) {
+ source = 't2_title'; key = t2;
+ patternId = `${vslug}::t2::${t2}`;
+ stats.via_t2_fallback++;
+ } else {
+ // last resort: pattern_name (so a product never goes key-less)
+ const pn = (row.pattern_name || '').toLowerCase().trim();
+ source = 'pattern_name'; key = pn || 'ungrouped';
+ patternId = `${vslug}::pn::${key}`;
+ stats.via_pattern_name_last_resort++;
+ }
+ }
+
+ if (texture) stats.texture_flagged++;
+ if (!row.image_url) stats.no_image++;
+
+ let g = groups.get(patternId);
+ if (!g) {
+ g = { pattern_id: patternId, vendor: row.vendor, source, key, texture, members: 0, samples: [] };
+ groups.set(patternId, g);
+ }
+ g.members++;
+ // a group is texture if ANY member is texture-tagged (trust mfr root, relax image gate)
+ if (texture) g.texture = true;
+ if (g.samples.length < 4) g.samples.push({ dw_sku: row.dw_sku, title: row.title });
+ }
+
+ const arr = [...groups.values()];
+ const multi = arr.filter(g => g.members >= 2);
+ const productsInMulti = multi.reduce((a, g) => a + g.members, 0);
+
+ // Image-confirm bucketing — faithful to DTD exception #3, which relaxes the gate
+ // ONLY when we "TRUST the mfr_sku ROOT". So:
+ // skip_gate = texture AND key came from mfr_root → trust root, no confirm
+ // needs_vision = texture AND key came from T2/pattern_name → fiber-safe LOCAL
+ // Ollama-vision pass (dHash false-splits random fibers; a
+ // T2 texture over-merge like generic-title Coordonné is NOT
+ // safe to auto-trust)
+ // needs_dhash = non-texture multi-group → standard color-invariant dHash guard
+ // SAFETY: a >HUGE_CAP single-root group is almost certainly a junk/over-merged
+ // root (proven: mirror mfr_sku has "SANCAR"/"TRUE" pollution → 178/81-member
+ // false roots). Such groups ALWAYS get confirmed even if texture+mfr_root.
+ const HUGE_CAP = 60;
+ const skipGateGroups = multi.filter(g => g.texture && g.source === 'mfr_root' && g.members <= HUGE_CAP);
+ const needsVisionGroups = multi.filter(g => g.texture && !(g.source === 'mfr_root' && g.members <= HUGE_CAP));
+ const dhashGroups = multi.filter(g => !g.texture);
+ const imageConfirmGroups = dhashGroups; // legacy alias for the dHash bucket
+ const imageConfirmProducts = dhashGroups.reduce((a, g) => a + g.members, 0);
+ const visionProducts = needsVisionGroups.reduce((a, g) => a + g.members, 0);
+ const textureMultiGroups = multi.filter(g => g.texture);
+
+ arr.sort((a, b) => b.members - a.members);
+
+ const report = {
+ generated_at: new Date().toISOString(),
+ mode: 'DRY-RUN (read-only; no Shopify call, no metafield write)',
+ active_products: rows.length,
+ mfr_map_entries: mfrMapSize,
+ used_mirror_mfr_column: USE_MIRROR_MFR,
+ counts: {
+ distinct_pattern_ids: arr.length,
+ multi_colorway_groups: multi.length,
+ products_in_multi_groups: productsInMulti,
+ grid_rows_collapsed: productsInMulti - multi.length,
+ pct_catalog_collapsed: ((productsInMulti - multi.length) / rows.length * 100).toFixed(1) + '%',
+ avg_group_size: (rows.length / arr.length).toFixed(2),
+ huge_groups_gt60: arr.filter(g => g.members > 60).length,
+ },
+ key_source_breakdown: stats,
+ texture_aware: {
+ texture_flagged_products: stats.texture_flagged,
+ texture_multi_colorway_groups: textureMultiGroups.length,
+ skip_gate_groups_mfr_root: skipGateGroups.length,
+ note: 'texture exception relaxes the image gate ONLY for mfr_root-sourced groups (trust the root). texture T2 over-merges are routed to the local-vision bucket, NOT auto-trusted.',
+ },
+ image_confirm: {
+ dhash_groups: dhashGroups.length,
+ dhash_products: imageConfirmProducts,
+ local_vision_groups: needsVisionGroups.length,
+ local_vision_products: visionProducts,
+ note: 'dHash bucket = non-texture multi-groups (pattern-image-verify.py, $0 local). local_vision bucket = texture multi-groups w/o a trusted mfr root (Ollama qwen2.5vl, $0). NEITHER run in dry-run.',
+ },
+ top_groups: arr.slice(0, 25).map(g => ({
+ n: g.members, vendor: g.vendor, source: g.source, texture: g.texture,
+ pattern_id: g.pattern_id, eg: g.samples.map(s => s.title),
+ })),
+ huge_groups_for_review: arr.filter(g => g.members > 60).slice(0, 20).map(g => ({
+ n: g.members, vendor: g.vendor, source: g.source, texture: g.texture, pattern_id: g.pattern_id,
+ eg: g.samples.map(s => s.title),
+ })),
+ };
+
+ const file = path.join(__dirname, 'pattern-id-backfill-report.json');
+ fs.writeFileSync(file, JSON.stringify(report, null, 2));
+
+ // ---- console digest ----
+ console.log('\n==================== PATTERN-ID BACKFILL · DRY-RUN ====================');
+ console.log('active products :', rows.length);
+ console.log('distinct pattern_ids :', report.counts.distinct_pattern_ids);
+ console.log('multi-colorway groups :', report.counts.multi_colorway_groups);
+ console.log('grid rows collapsed :', report.counts.grid_rows_collapsed,
+ '(' + report.counts.pct_catalog_collapsed + ' of catalog)');
+ console.log('avg group size :', report.counts.avg_group_size);
+ console.log('huge groups (>60, inspect) :', report.counts.huge_groups_gt60);
+ console.log('--- key source ---');
+ console.log(' via mfr_root (primary) :', stats.via_mfr_root);
+ console.log(' via T2 title (fallback) :', stats.via_t2_fallback);
+ console.log(' via pattern_name (last) :', stats.via_pattern_name_last_resort);
+ console.log('--- texture-aware exception ---');
+ console.log(' texture-flagged products :', stats.texture_flagged);
+ console.log(' texture multi-groups :', textureMultiGroups.length);
+ console.log(' skip-gate (mfr_root only) :', skipGateGroups.length, 'groups');
+ console.log('--- image-confirm guard (NOT run in dry-run) ---');
+ console.log(' dHash groups (non-texture) :', dhashGroups.length, '·', imageConfirmProducts, 'products');
+ console.log(' local-vision (texture T2) :', needsVisionGroups.length, '·', visionProducts, 'products');
+ console.log(' products missing image :', stats.no_image);
+ console.log('--- top 5 largest groups ---');
+ report.top_groups.slice(0, 5).forEach(g =>
+ console.log(` ${g.n}× [${g.vendor}] ${g.source}${g.texture ? ' (texture)' : ''} ${g.pattern_id}`));
+ console.log('\nFull report →', file);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/tools/pattern-texture-vision-stub.py b/tools/pattern-texture-vision-stub.py
new file mode 100644
index 0000000..50c9144
--- /dev/null
+++ b/tools/pattern-texture-vision-stub.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+"""DESIGN-ONLY STUB — texture-aware local-vision confirm for pattern grouping.
+
+DTD exception #3: the color-invariant dHash FALSE-SPLITS organic textures
+(grasscloth/linen/raffia/cork — fibers are random; Aloha Grass → 7 false
+clusters). For texture multi-colorway groups that DON'T have a trusted mfr_sku
+root (or that are suspiciously huge), we confirm with a FREE local Ollama
+vision pass (qwen2.5vl on Mac1/Mac2) instead of the dHash gate.
+
+THIS IS A STUB. It is NOT wired to run a 74k job. It documents the intended
+call shape so the gated apply step can implement it. Cost = $0 (local Ollama).
+
+Intended flow (per candidate group, post mfr_root/T2 keying):
+ 1. Pull the group's member image_urls (read-only, from the mirror).
+ 2. For each pair (or representative-vs-member), ask qwen2.5vl ONE question:
+ "Are these two images the SAME woven/textured pattern in different
+ colorways, ignoring color? Answer YES or NO."
+ -> tolerant of random-fiber noise that breaks dHash; sensitive to a real
+ motif/weave-structure change (so a generic-title over-merge still splits).
+ 3. Members the model says match the representative stay in the cluster;
+ non-matches split into their own pattern_id (suffix ::v2, ::v3 …).
+
+Hard gotchas (from MEMORY):
+ - Normalize each image Pillow -> JPEG before sending to Ollama; it rejects
+ CMYK/WebP (word-salad otherwise).
+ - Use OLLAMA_KV_CACHE_TYPE=f16 for vision (q4_0 KV cache breaks vision).
+ - Mac1 text inference can wedge while vision still serves; prove health via
+ /api/generate num_predict:1 before a batch, fail over to Mac2 127.0.0.1.
+ - This is the SAME local-hybrid pattern as enrich-ai-tags (qwen2.5vl), $0.
+
+Reference (do not duplicate): tools/pattern-image-verify.py is the dHash gate
+for the NON-texture bucket; this stub is its texture-safe sibling.
+"""
+import os, sys
+
+OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
+VISION_MODEL = os.environ.get("VISION_MODEL", "qwen2.5vl")
+
+PROMPT = ("Are these two wallcovering images the SAME woven/textured pattern in "
+ "different colorways, ignoring color and minor fiber noise? "
+ "Answer with a single word: YES or NO.")
+
+def confirm_texture_group(member_urls):
+ """STUB — returns clusters. Not implemented; raises so nobody runs it blind."""
+ raise NotImplementedError(
+ "Design-only stub. Implement during the GATED apply step: normalize each "
+ "image to JPEG (Pillow), POST to %s /api/chat with model=%s and PROMPT, "
+ "parse YES/NO, cluster. $0 local. See module docstring." % (OLLAMA_URL, VISION_MODEL))
+
+if __name__ == "__main__":
+ print(__doc__)
+ print(f"\n[stub] OLLAMA_URL={OLLAMA_URL} VISION_MODEL={VISION_MODEL} cost=$0 (local)")
+ print("[stub] Not runnable — design reference only. Exiting 0.")
+ sys.exit(0)
← c76eef6 auto-save: 2026-06-25T23:37:59 (1 files) — tools/
·
back to Dw Pairs Well
·
WS-3: /api/colorways + /api/collection-grouped (interim T2 g 901bbdd →