← back to Designerwallcoverings
TK-11076: title-anchored colorway parser + fleet retag driver + rollback (canary DRY-RUN, no Shopify writes)
801f3c51885a45df75dd62b5adf792956d1bcadf · 2026-09-01 13:32:23 -0700 · Steve Abrams
- parse-colorway.mjs: reusable mfr-colorway parser, both grammars (dash ' By <V>', pipe ' | <V>',
comma-form), material-suffix + marketing-tail strip, confidence-gate SKIP on fused/no-delimiter
titles. 28/28 self-test.
- tk11076-colorway-retag.mjs: DRY-RUN/canary driver -> custom.real_color_name metafield + color:<cw>
tag; strips only per-product AI-palette color strays; per-vendor rollback file; per-item verify;
90s gap/25 throttle on --apply (gated, not run this pass).
- Canary: 26 examined, 23 WRITE, 3 SKIP. Named checks pass (Cole&Son Charcoal, RebelWalls Rust,
Aquila Rust->SKIP). Fleet: 9155/18062 confident, 8907 SKIP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/colorway-title-anchor/out/tk11076-addressable-by-vendor.jsonA scripts/colorway-title-anchor/out/tk11076-canary-before-after.txtA scripts/colorway-title-anchor/out/tk11076-canary-mixed-diff.jsonA scripts/colorway-title-anchor/out/tk11076-nonchromatic-tails.jsonA scripts/colorway-title-anchor/parse-colorway.mjsA scripts/colorway-title-anchor/tk11076-colorway-retag.mjsA scripts/colorway-title-anchor/tk11076-rollback.mjs
Diff
commit 801f3c51885a45df75dd62b5adf792956d1bcadf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 13:32:23 2026 -0700
TK-11076: title-anchored colorway parser + fleet retag driver + rollback (canary DRY-RUN, no Shopify writes)
- parse-colorway.mjs: reusable mfr-colorway parser, both grammars (dash ' By <V>', pipe ' | <V>',
comma-form), material-suffix + marketing-tail strip, confidence-gate SKIP on fused/no-delimiter
titles. 28/28 self-test.
- tk11076-colorway-retag.mjs: DRY-RUN/canary driver -> custom.real_color_name metafield + color:<cw>
tag; strips only per-product AI-palette color strays; per-vendor rollback file; per-item verify;
90s gap/25 throttle on --apply (gated, not run this pass).
- Canary: 26 examined, 23 WRITE, 3 SKIP. Named checks pass (Cole&Son Charcoal, RebelWalls Rust,
Aquila Rust->SKIP). Fleet: 9155/18062 confident, 8907 SKIP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../out/tk11076-addressable-by-vendor.json | 585 ++++++++++++++++++++
.../out/tk11076-canary-before-after.txt | 217 ++++++++
.../out/tk11076-canary-mixed-diff.json | 612 +++++++++++++++++++++
.../out/tk11076-nonchromatic-tails.json | 32 ++
scripts/colorway-title-anchor/parse-colorway.mjs | 173 ++++++
.../tk11076-colorway-retag.mjs | 304 ++++++++++
scripts/colorway-title-anchor/tk11076-rollback.mjs | 33 ++
7 files changed, 1956 insertions(+)
diff --git a/scripts/colorway-title-anchor/out/tk11076-addressable-by-vendor.json b/scripts/colorway-title-anchor/out/tk11076-addressable-by-vendor.json
new file mode 100644
index 0000000..cc774c5
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/tk11076-addressable-by-vendor.json
@@ -0,0 +1,585 @@
+{
+ "total": 18062,
+ "write": 9155,
+ "skip": 8907,
+ "reasons": {
+ "no-delimiter": 8836,
+ "too-long": 71
+ },
+ "forms": {
+ "dash": 8545,
+ "comma": 610
+ },
+ "per_vendor": [
+ {
+ "vendor": "Phillipe Romano",
+ "write": 428,
+ "skip": 1949,
+ "total": 2377
+ },
+ {
+ "vendor": "China Seas",
+ "write": 568,
+ "skip": 928,
+ "total": 1496
+ },
+ {
+ "vendor": "Rebel Walls",
+ "write": 546,
+ "skip": 916,
+ "total": 1462
+ },
+ {
+ "vendor": "Phillip Jeffries",
+ "write": 1046,
+ "skip": 166,
+ "total": 1212
+ },
+ {
+ "vendor": "Coordonné",
+ "write": 0,
+ "skip": 875,
+ "total": 875
+ },
+ {
+ "vendor": "Kravet",
+ "write": 711,
+ "skip": 94,
+ "total": 805
+ },
+ {
+ "vendor": "Malibu Wallpaper",
+ "write": 674,
+ "skip": 41,
+ "total": 715
+ },
+ {
+ "vendor": "Hollywood Wallcoverings",
+ "write": 343,
+ "skip": 283,
+ "total": 626
+ },
+ {
+ "vendor": "Cole & Son",
+ "write": 585,
+ "skip": 1,
+ "total": 586
+ },
+ {
+ "vendor": "DW Bespoke Studio",
+ "write": 88,
+ "skip": 495,
+ "total": 583
+ },
+ {
+ "vendor": "Osborne & Little",
+ "write": 1,
+ "skip": 499,
+ "total": 500
+ },
+ {
+ "vendor": "Scalamandre Wallpaper",
+ "write": 477,
+ "skip": 0,
+ "total": 477
+ },
+ {
+ "vendor": "Sandberg",
+ "write": 103,
+ "skip": 323,
+ "total": 426
+ },
+ {
+ "vendor": "Designer Wallcoverings",
+ "write": 109,
+ "skip": 297,
+ "total": 406
+ },
+ {
+ "vendor": "William Morris",
+ "write": 0,
+ "skip": 338,
+ "total": 338
+ },
+ {
+ "vendor": "Anna French",
+ "write": 331,
+ "skip": 0,
+ "total": 331
+ },
+ {
+ "vendor": "Harlequin",
+ "write": 325,
+ "skip": 0,
+ "total": 325
+ },
+ {
+ "vendor": "Maya Romanoff",
+ "write": 313,
+ "skip": 7,
+ "total": 320
+ },
+ {
+ "vendor": "Mind the Gap",
+ "write": 194,
+ "skip": 94,
+ "total": 288
+ },
+ {
+ "vendor": "Romo",
+ "write": 283,
+ "skip": 0,
+ "total": 283
+ },
+ {
+ "vendor": "Surface Stick",
+ "write": 278,
+ "skip": 0,
+ "total": 278
+ },
+ {
+ "vendor": "Glitter Walls",
+ "write": 228,
+ "skip": 0,
+ "total": 228
+ },
+ {
+ "vendor": "Jeffrey Stevens",
+ "write": 0,
+ "skip": 210,
+ "total": 210
+ },
+ {
+ "vendor": "Versace",
+ "write": 24,
+ "skip": 184,
+ "total": 208
+ },
+ {
+ "vendor": "Gaston Y Daniela",
+ "write": 203,
+ "skip": 0,
+ "total": 203
+ },
+ {
+ "vendor": "Clarke And Clarke",
+ "write": 190,
+ "skip": 0,
+ "total": 190
+ },
+ {
+ "vendor": "Graham & Brown",
+ "write": 1,
+ "skip": 177,
+ "total": 178
+ },
+ {
+ "vendor": "Innovations USA",
+ "write": 0,
+ "skip": 157,
+ "total": 157
+ },
+ {
+ "vendor": "Designers Guild",
+ "write": 5,
+ "skip": 117,
+ "total": 122
+ },
+ {
+ "vendor": "Ralph Lauren",
+ "write": 113,
+ "skip": 0,
+ "total": 113
+ },
+ {
+ "vendor": "Nina Campbell",
+ "write": 104,
+ "skip": 0,
+ "total": 104
+ },
+ {
+ "vendor": "British Walls",
+ "write": 93,
+ "skip": 9,
+ "total": 102
+ },
+ {
+ "vendor": "Mid Century Modern",
+ "write": 0,
+ "skip": 97,
+ "total": 97
+ },
+ {
+ "vendor": "Wolf Gordon",
+ "write": 93,
+ "skip": 1,
+ "total": 94
+ },
+ {
+ "vendor": "GP & J Baker",
+ "write": 93,
+ "skip": 0,
+ "total": 93
+ },
+ {
+ "vendor": "Marburg",
+ "write": 72,
+ "skip": 19,
+ "total": 91
+ },
+ {
+ "vendor": "Brunschwig & Fils",
+ "write": 78,
+ "skip": 0,
+ "total": 78
+ },
+ {
+ "vendor": "Fentucci",
+ "write": 48,
+ "skip": 20,
+ "total": 68
+ },
+ {
+ "vendor": "Brand McKenzie",
+ "write": 15,
+ "skip": 48,
+ "total": 63
+ },
+ {
+ "vendor": "Pixels",
+ "write": 57,
+ "skip": 0,
+ "total": 57
+ },
+ {
+ "vendor": "Laura Ashley Wallpaper",
+ "write": 0,
+ "skip": 53,
+ "total": 53
+ },
+ {
+ "vendor": "Ultrasuede",
+ "write": 10,
+ "skip": 36,
+ "total": 46
+ },
+ {
+ "vendor": "MC Escher Wallpaper",
+ "write": 0,
+ "skip": 46,
+ "total": 46
+ },
+ {
+ "vendor": "Luxury Murals",
+ "write": 0,
+ "skip": 42,
+ "total": 42
+ },
+ {
+ "vendor": "Hospitality Wallcoverings",
+ "write": 0,
+ "skip": 40,
+ "total": 40
+ },
+ {
+ "vendor": "Steve Abrams Studios",
+ "write": 18,
+ "skip": 21,
+ "total": 39
+ },
+ {
+ "vendor": "Donghia",
+ "write": 37,
+ "skip": 0,
+ "total": 37
+ },
+ {
+ "vendor": "Sancar",
+ "write": 0,
+ "skip": 37,
+ "total": 37
+ },
+ {
+ "vendor": "Thibaut",
+ "write": 16,
+ "skip": 20,
+ "total": 36
+ },
+ {
+ "vendor": "Roberto Cavalli Wallpaper",
+ "write": 0,
+ "skip": 35,
+ "total": 35
+ },
+ {
+ "vendor": "DW Home",
+ "write": 12,
+ "skip": 23,
+ "total": 35
+ },
+ {
+ "vendor": "Nobilis",
+ "write": 3,
+ "skip": 31,
+ "total": 34
+ },
+ {
+ "vendor": "Marimekko Exclusive",
+ "write": 0,
+ "skip": 33,
+ "total": 33
+ },
+ {
+ "vendor": "Elitis",
+ "write": 0,
+ "skip": 30,
+ "total": 30
+ },
+ {
+ "vendor": "Daisy Bennett",
+ "write": 28,
+ "skip": 0,
+ "total": 28
+ },
+ {
+ "vendor": "Mulberry",
+ "write": 28,
+ "skip": 0,
+ "total": 28
+ },
+ {
+ "vendor": "Natural Paint",
+ "write": 26,
+ "skip": 0,
+ "total": 26
+ },
+ {
+ "vendor": "Threads",
+ "write": 25,
+ "skip": 0,
+ "total": 25
+ },
+ {
+ "vendor": "Architectural Fabrics",
+ "write": 23,
+ "skip": 0,
+ "total": 23
+ },
+ {
+ "vendor": "Groundworks",
+ "write": 21,
+ "skip": 0,
+ "total": 21
+ },
+ {
+ "vendor": "Lee Jofa",
+ "write": 20,
+ "skip": 0,
+ "total": 20
+ },
+ {
+ "vendor": "Retro Walls",
+ "write": 9,
+ "skip": 6,
+ "total": 15
+ },
+ {
+ "vendor": "Missoni Wallpaper",
+ "write": 13,
+ "skip": 2,
+ "total": 15
+ },
+ {
+ "vendor": "Madagascar Walls",
+ "write": 11,
+ "skip": 2,
+ "total": 13
+ },
+ {
+ "vendor": "Traditional Whimsy",
+ "write": 2,
+ "skip": 10,
+ "total": 12
+ },
+ {
+ "vendor": "LA Walls",
+ "write": 0,
+ "skip": 12,
+ "total": 12
+ },
+ {
+ "vendor": "Franquemont London",
+ "write": 0,
+ "skip": 11,
+ "total": 11
+ },
+ {
+ "vendor": "Glambeads by DW",
+ "write": 0,
+ "skip": 10,
+ "total": 10
+ },
+ {
+ "vendor": "Los Angeles Fabrics",
+ "write": 9,
+ "skip": 0,
+ "total": 9
+ },
+ {
+ "vendor": "Hand Crafted",
+ "write": 2,
+ "skip": 7,
+ "total": 9
+ },
+ {
+ "vendor": "Koroseal",
+ "write": 9,
+ "skip": 0,
+ "total": 9
+ },
+ {
+ "vendor": "Graduate Collection",
+ "write": 0,
+ "skip": 8,
+ "total": 8
+ },
+ {
+ "vendor": "Arte International",
+ "write": 4,
+ "skip": 3,
+ "total": 7
+ },
+ {
+ "vendor": "Glass Beaded",
+ "write": 3,
+ "skip": 3,
+ "total": 6
+ },
+ {
+ "vendor": "Dolce & Gabbana",
+ "write": 0,
+ "skip": 6,
+ "total": 6
+ },
+ {
+ "vendor": "Hollywood Acoustical",
+ "write": 0,
+ "skip": 5,
+ "total": 5
+ },
+ {
+ "vendor": "Graduate Collection UK",
+ "write": 2,
+ "skip": 2,
+ "total": 4
+ },
+ {
+ "vendor": "Vahallan",
+ "write": 0,
+ "skip": 4,
+ "total": 4
+ },
+ {
+ "vendor": "Contrado",
+ "write": 0,
+ "skip": 4,
+ "total": 4
+ },
+ {
+ "vendor": "Primo Leathers",
+ "write": 0,
+ "skip": 3,
+ "total": 3
+ },
+ {
+ "vendor": "Malibu",
+ "write": 1,
+ "skip": 2,
+ "total": 3
+ },
+ {
+ "vendor": "Sacha Walckhoff",
+ "write": 0,
+ "skip": 2,
+ "total": 2
+ },
+ {
+ "vendor": "Patty Madden",
+ "write": 0,
+ "skip": 2,
+ "total": 2
+ },
+ {
+ "vendor": "Christian Lacroix Europe",
+ "write": 0,
+ "skip": 2,
+ "total": 2
+ },
+ {
+ "vendor": "Fabricut",
+ "write": 0,
+ "skip": 2,
+ "total": 2
+ },
+ {
+ "vendor": "Holland and Sherry",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Winfield Thybony",
+ "write": 1,
+ "skip": 0,
+ "total": 1
+ },
+ {
+ "vendor": "Steven Abrams Photography",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Catchii Netherlands Europe",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Rebecca Moses",
+ "write": 1,
+ "skip": 0,
+ "total": 1
+ },
+ {
+ "vendor": "Pierre Frey",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Tres Tintas",
+ "write": 1,
+ "skip": 0,
+ "total": 1
+ },
+ {
+ "vendor": "Morris and Company",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Farrow & Ball",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ },
+ {
+ "vendor": "Studio Ditte",
+ "write": 0,
+ "skip": 1,
+ "total": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/colorway-title-anchor/out/tk11076-canary-before-after.txt b/scripts/colorway-title-anchor/out/tk11076-canary-before-after.txt
new file mode 100644
index 0000000..fec0301
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/tk11076-canary-before-after.txt
@@ -0,0 +1,217 @@
+TK-11076 COLORWAY CANARY — BEFORE/AFTER (DRY-RUN, no Shopify writes)
+generated 2026-09-01T20:32:04.145Z | metafield target: custom.real_color_name
+examined 26 WRITE 23 SKIP 3
+========================================================================================================================
+
+VENDOR: Rebel Walls FORM: pipe/dash
+ TITLE (untouched) : Botanical Bliss - Rust | Rebel Walls
+ OLD color-related tags : [Brown]
+ PARSED colorway : Rust
+ NEW color: tag : color:Rust
+ metafield real_color_name: (empty) -> Rust
+ PALETTE strays stripped : [Brown]
+ DECISION : WRITE
+
+VENDOR: Clarke And Clarke FORM: dash-by/dash
+ TITLE (untouched) : Acanthus - Slate Wp Grey By Clarke And Clarke | Clarke & Clarke William Morris Wallcovering | Botanical & Floral Wallcovering Print
+ OLD color-related tags : [Pewter]
+ PARSED colorway : Slate Wp Grey
+ NEW color: tag : color:Slate Wp Grey
+ metafield real_color_name: (empty) -> Slate Wp Grey
+ PALETTE strays stripped : [Pewter]
+ DECISION : WRITE
+
+VENDOR: Clarke And Clarke FORM: dash-by/dash
+ TITLE (untouched) : Kruger - Flame By Clarke And Clarke | Animalia By Emma J Shipley For C&C | Animal/Insects Wallcovering Print
+ OLD color-related tags : [Orange]
+ PARSED colorway : Flame
+ NEW color: tag : color:Flame
+ metafield real_color_name: (empty) -> Flame
+ PALETTE strays stripped : [Orange]
+ DECISION : WRITE
+
+VENDOR: Brunschwig & Fils FORM: dash-by/dash
+ TITLE (untouched) : Bibliotheque - Greige Multi By Brunschwig & Fils | Volume 56 | Figurative Wallcovering Print
+ OLD color-related tags : [White]
+ PARSED colorway : Greige Multi
+ NEW color: tag : color:Greige Multi
+ metafield real_color_name: (empty) -> Greige Multi
+ PALETTE strays stripped : [White]
+ DECISION : WRITE
+
+VENDOR: Brunschwig & Fils FORM: dash-by/dash
+ TITLE (untouched) : Treillage Sidewall - Silver By Brunschwig & Fils | Volume 56 Print
+ OLD color-related tags : [Gray, Light Gray, White]
+ PARSED colorway : Silver
+ NEW color: tag : color:Silver
+ metafield real_color_name: Beige -> Silver
+ PALETTE strays stripped : [Gray, Light Gray, White]
+ DECISION : WRITE
+
+VENDOR: Brunschwig & Fils FORM: dash-by/dash
+ TITLE (untouched) : Wood - Birch Grey By Brunschwig & Fils | Volume 56 |Solid Texture Wallcovering Print
+ OLD color-related tags : [Light Gray, Oatmeal]
+ PARSED colorway : Birch Grey
+ NEW color: tag : color:Birch Grey
+ metafield real_color_name: (empty) -> Birch Grey
+ PALETTE strays stripped : [Light Gray, Oatmeal]
+ DECISION : WRITE
+
+VENDOR: Lee Jofa FORM: pipe/dash
+ TITLE (untouched) : Ellis Paper - Bayside Wallcovering | Lee Jofa
+ OLD color-related tags : []
+ PARSED colorway : Bayside
+ NEW color: tag : color:Bayside
+ metafield real_color_name: (empty) -> Bayside
+ PALETTE strays stripped : []
+ DECISION : WRITE
+
+VENDOR: GP & J Baker FORM: dash-by/dash
+ TITLE (untouched) : Ramayana - Emerald By G P & J Baker | Signature Ii Wallcoverings |Toile Wallcovering Print
+ OLD color-related tags : [Pale Beige]
+ PARSED colorway : Emerald
+ NEW color: tag : color:Emerald
+ metafield real_color_name: (empty) -> Emerald
+ PALETTE strays stripped : [Pale Beige]
+ DECISION : WRITE
+
+VENDOR: Groundworks FORM: pipe/dash
+ TITLE (untouched) : Channels Paper - Ebony/Iv Wallcovering | Phillipe Romano
+ OLD color-related tags : [White]
+ PARSED colorway : Ebony/Iv
+ NEW color: tag : color:Ebony/Iv
+ metafield real_color_name: (empty) -> Ebony/Iv
+ PALETTE strays stripped : [White]
+ DECISION : WRITE
+
+VENDOR: Cole & Son FORM: pipe/dash
+ TITLE (untouched) : Matrinah - Old Olive Green | Cole & Son
+ OLD color-related tags : [color:OLD OLIVE, Sage, Taupe]
+ PARSED colorway : Old Olive Green
+ NEW color: tag : color:Old Olive Green
+ metafield real_color_name: OLD OLIVE -> Old Olive Green
+ PALETTE strays stripped : [Sage, Taupe]
+ DECISION : WRITE
+
+VENDOR: Cole & Son FORM: pipe/dash
+ TITLE (untouched) : Thistle - Lilac/Cerise/White Multi | Cole & Son
+ OLD color-related tags : [Lilac/Cerise/White, Linen, Taupe]
+ PARSED colorway : Lilac/Cerise/White Multi
+ NEW color: tag : color:Lilac/Cerise/White Multi
+ metafield real_color_name: LILAC/CERISE/WHITE -> Lilac/Cerise/White Multi
+ PALETTE strays stripped : [Lilac/Cerise/White, Linen, Taupe]
+ DECISION : WRITE
+
+VENDOR: Cole & Son FORM: pipe/dash
+ TITLE (untouched) : Savuti - Dark Petrol Multi | Cole & Son
+ OLD color-related tags : [color:DARK PETROL MULTI, Taupe]
+ PARSED colorway : Dark Petrol Multi
+ NEW color: tag : color:Dark Petrol Multi
+ metafield real_color_name: DARK PETROL MULTI -> Dark Petrol Multi
+ PALETTE strays stripped : [Taupe]
+ DECISION : WRITE
+
+VENDOR: Rebel Walls FORM: pipe/dash
+ TITLE (untouched) : Winding Spring - Black | Rebel Walls
+ OLD color-related tags : [Charcoal]
+ PARSED colorway : Black
+ NEW color: tag : color:Black
+ metafield real_color_name: (empty) -> Black
+ PALETTE strays stripped : [Charcoal]
+ DECISION : WRITE
+
+VENDOR: Rebel Walls FORM: pipe/dash
+ TITLE (untouched) : Leo - King of the Circus | Rebel Walls
+ OLD color-related tags : [Silver, White]
+ PARSED colorway : King Of The Circus
+ NEW color: tag : color:King Of The Circus
+ metafield real_color_name: (empty) -> King Of The Circus
+ PALETTE strays stripped : [Silver, White]
+ DECISION : WRITE
+
+VENDOR: Rebel Walls FORM: pipe/dash
+ TITLE (untouched) : Lily Pond - Rainbow | Rebel Walls
+ OLD color-related tags : [Sage, White]
+ PARSED colorway : Rainbow
+ NEW color: tag : color:Rainbow
+ metafield real_color_name: (empty) -> Rainbow
+ PALETTE strays stripped : [Sage, White]
+ DECISION : WRITE
+
+VENDOR: Maya Romanoff FORM: pipe/dash
+ TITLE (untouched) : Grand Scale Ajiro Chevron - Bamboo | Maya Romanoff
+ OLD color-related tags : [Beige, Sand]
+ PARSED colorway : Bamboo
+ NEW color: tag : color:Bamboo
+ metafield real_color_name: Bamboo -> Bamboo
+ PALETTE strays stripped : [Beige, Sand]
+ DECISION : WRITE
+
+VENDOR: Maya Romanoff FORM: pipe/dash
+ TITLE (untouched) : Conduit - Tempered Tan | Maya Romanoff
+ OLD color-related tags : [Oatmeal]
+ PARSED colorway : Tempered Tan
+ NEW color: tag : color:Tempered Tan
+ metafield real_color_name: (empty) -> Tempered Tan
+ PALETTE strays stripped : [Oatmeal]
+ DECISION : WRITE
+
+VENDOR: Mind the Gap FORM: pipe/dash
+ TITLE (untouched) : Obscure - Beige Wallcovering | Mind the Gap
+ OLD color-related tags : [Black, Light Gray]
+ PARSED colorway : Beige
+ NEW color: tag : color:Beige
+ metafield real_color_name: (empty) -> Beige
+ PALETTE strays stripped : [Black, Light Gray]
+ DECISION : WRITE
+
+VENDOR: Mind the Gap FORM: pipe/dash
+ TITLE (untouched) : Floral Painting - Beige Wallcovering | Mind the Gap
+ OLD color-related tags : [Black]
+ PARSED colorway : Beige
+ NEW color: tag : color:Beige
+ metafield real_color_name: (empty) -> Beige
+ PALETTE strays stripped : [Black]
+ DECISION : WRITE
+
+VENDOR: Romo FORM: pipe/dash
+ TITLE (untouched) : Raya Silk Embossed - Caper Natural Wallcovering | Romo
+ OLD color-related tags : [Taupe]
+ PARSED colorway : Caper Natural
+ NEW color: tag : color:Caper Natural
+ metafield real_color_name: Caper -> Caper Natural
+ PALETTE strays stripped : [Taupe]
+ DECISION : WRITE
+
+VENDOR: Romo FORM: pipe/dash
+ TITLE (untouched) : Monazite - Cocktail Wallcovering | Romo
+ OLD color-related tags : [Taupe]
+ PARSED colorway : Cocktail
+ NEW color: tag : color:Cocktail
+ metafield real_color_name: Cocktail -> Cocktail
+ PALETTE strays stripped : [Taupe]
+ DECISION : WRITE
+
+VENDOR: Phillip Jeffries FORM: bare/dash
+ TITLE (untouched) : Star Dust Silk - Supernova White
+ OLD color-related tags : []
+ PARSED colorway : Supernova White
+ NEW color: tag : color:Supernova White
+ metafield real_color_name: (empty) -> Supernova White
+ PALETTE strays stripped : []
+ DECISION : WRITE
+
+VENDOR: Phillip Jeffries FORM: bare/dash
+ TITLE (untouched) : Cabana Weave - Thundercloud
+ OLD color-related tags : [White]
+ PARSED colorway : Thundercloud
+ NEW color: tag : color:Thundercloud
+ metafield real_color_name: (empty) -> Thundercloud
+ PALETTE strays stripped : [White]
+ DECISION : WRITE
+
+------------------------------------------------------------
+SKIPS (confidence gate — left AS-IS):
+ SKIP [Rebel Walls] Arlon Teal | Rebel Walls reason=no-delimiter (pipe/no-delim)
+ SKIP [Rebel Walls] Patinated Diamonds | Rebel Walls reason=no-delimiter (pipe/no-delim)
+ SKIP [Rebel Walls] Memory Lane | Rebel Walls reason=no-delimiter (pipe/no-delim)
diff --git a/scripts/colorway-title-anchor/out/tk11076-canary-mixed-diff.json b/scripts/colorway-title-anchor/out/tk11076-canary-mixed-diff.json
new file mode 100644
index 0000000..92894c7
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/tk11076-canary-mixed-diff.json
@@ -0,0 +1,612 @@
+{
+ "ts": "2026-09-01T20:32:04.145Z",
+ "mode": "DRY-RUN",
+ "scope": "canary-mixed",
+ "live_products_examined": 26,
+ "would_write": 23,
+ "would_skip": 3,
+ "metafield_target": "custom.real_color_name",
+ "before_after": [
+ {
+ "handle": "botanical-bliss-rust-wallcoverings-dwrw-360566",
+ "vendor": "Rebel Walls",
+ "title": "Botanical Bliss - Rust | Rebel Walls",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Brown"
+ ],
+ "parsed_colorway": "Rust",
+ "new_color_tag": "color:Rust",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Rust",
+ "palette_strays_stripped": [
+ "Brown"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Rust"
+ ],
+ "tags_removed": [
+ "Brown"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-127974",
+ "vendor": "Clarke And Clarke",
+ "title": "Acanthus - Slate Wp Grey By Clarke And Clarke | Clarke & Clarke William Morris Wallcovering | Botanical & Floral Wallcovering Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "Pewter"
+ ],
+ "parsed_colorway": "Slate Wp Grey",
+ "new_color_tag": "color:Slate Wp Grey",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Slate Wp Grey",
+ "palette_strays_stripped": [
+ "Pewter"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Slate Wp Grey"
+ ],
+ "tags_removed": [
+ "Pewter"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-127734",
+ "vendor": "Clarke And Clarke",
+ "title": "Kruger - Flame By Clarke And Clarke | Animalia By Emma J Shipley For C&C | Animal/Insects Wallcovering Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "Orange"
+ ],
+ "parsed_colorway": "Flame",
+ "new_color_tag": "color:Flame",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Flame",
+ "palette_strays_stripped": [
+ "Orange"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Flame"
+ ],
+ "tags_removed": [
+ "Orange"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-101530",
+ "vendor": "Brunschwig & Fils",
+ "title": "Bibliotheque - Greige Multi By Brunschwig & Fils | Volume 56 | Figurative Wallcovering Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "White"
+ ],
+ "parsed_colorway": "Greige Multi",
+ "new_color_tag": "color:Greige Multi",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Greige Multi",
+ "palette_strays_stripped": [
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Greige Multi"
+ ],
+ "tags_removed": [
+ "White"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-102027",
+ "vendor": "Brunschwig & Fils",
+ "title": "Treillage Sidewall - Silver By Brunschwig & Fils | Volume 56 Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "Gray",
+ "Light Gray",
+ "White"
+ ],
+ "parsed_colorway": "Silver",
+ "new_color_tag": "color:Silver",
+ "old_metafield_real_color_name": "Beige",
+ "new_metafield_real_color_name": "Silver",
+ "palette_strays_stripped": [
+ "Gray",
+ "Light Gray",
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Silver"
+ ],
+ "tags_removed": [
+ "Gray",
+ "Light Gray",
+ "White"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-102011",
+ "vendor": "Brunschwig & Fils",
+ "title": "Wood - Birch Grey By Brunschwig & Fils | Volume 56 |Solid Texture Wallcovering Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "Light Gray",
+ "Oatmeal"
+ ],
+ "parsed_colorway": "Birch Grey",
+ "new_color_tag": "color:Birch Grey",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Birch Grey",
+ "palette_strays_stripped": [
+ "Light Gray",
+ "Oatmeal"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Birch Grey"
+ ],
+ "tags_removed": [
+ "Light Gray",
+ "Oatmeal"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "ellis-paper-bayside-wallcovering-lee-jofa",
+ "vendor": "Lee Jofa",
+ "title": "Ellis Paper - Bayside Wallcovering | Lee Jofa",
+ "form": "pipe/dash",
+ "old_color_related_tags": [],
+ "parsed_colorway": "Bayside",
+ "new_color_tag": "color:Bayside",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Bayside",
+ "palette_strays_stripped": [],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Bayside"
+ ],
+ "tags_removed": [],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-137742",
+ "vendor": "GP & J Baker",
+ "title": "Ramayana - Emerald By G P & J Baker | Signature Ii Wallcoverings |Toile Wallcovering Print",
+ "form": "dash-by/dash",
+ "old_color_related_tags": [
+ "Pale Beige"
+ ],
+ "parsed_colorway": "Emerald",
+ "new_color_tag": "color:Emerald",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Emerald",
+ "palette_strays_stripped": [
+ "Pale Beige"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Emerald"
+ ],
+ "tags_removed": [
+ "Pale Beige"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "channels-paper-ebony-iv-wallcovering-phillipe-romano-1",
+ "vendor": "Groundworks",
+ "title": "Channels Paper - Ebony/Iv Wallcovering | Phillipe Romano",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "White"
+ ],
+ "parsed_colorway": "Ebony/Iv",
+ "new_color_tag": "color:Ebony/Iv",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Ebony/Iv",
+ "palette_strays_stripped": [
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Ebony/Iv"
+ ],
+ "tags_removed": [
+ "White"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-gc2080a23",
+ "vendor": "Cole & Son",
+ "title": "Matrinah - Old Olive Green | Cole & Son",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "color:OLD OLIVE",
+ "Sage",
+ "Taupe"
+ ],
+ "parsed_colorway": "Old Olive Green",
+ "new_color_tag": "color:Old Olive Green",
+ "old_metafield_real_color_name": "OLD OLIVE",
+ "new_metafield_real_color_name": "Old Olive Green",
+ "palette_strays_stripped": [
+ "Sage",
+ "Taupe"
+ ],
+ "wrong_facets_stripped": [
+ "color:OLD OLIVE"
+ ],
+ "tags_added": [
+ "color:Old Olive Green"
+ ],
+ "tags_removed": [
+ "color:OLD OLIVE",
+ "Sage",
+ "Taupe"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-gbeb55b32",
+ "vendor": "Cole & Son",
+ "title": "Thistle - Lilac/Cerise/White Multi | Cole & Son",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Lilac/Cerise/White",
+ "Linen",
+ "Taupe"
+ ],
+ "parsed_colorway": "Lilac/Cerise/White Multi",
+ "new_color_tag": "color:Lilac/Cerise/White Multi",
+ "old_metafield_real_color_name": "LILAC/CERISE/WHITE",
+ "new_metafield_real_color_name": "Lilac/Cerise/White Multi",
+ "palette_strays_stripped": [
+ "Lilac/Cerise/White",
+ "Linen",
+ "Taupe"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Lilac/Cerise/White Multi"
+ ],
+ "tags_removed": [
+ "Lilac/Cerise/White",
+ "Linen",
+ "Taupe"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwkk-ge2422bc2",
+ "vendor": "Cole & Son",
+ "title": "Savuti - Dark Petrol Multi | Cole & Son",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "color:DARK PETROL MULTI",
+ "Taupe"
+ ],
+ "parsed_colorway": "Dark Petrol Multi",
+ "new_color_tag": "color:Dark Petrol Multi",
+ "old_metafield_real_color_name": "DARK PETROL MULTI",
+ "new_metafield_real_color_name": "Dark Petrol Multi",
+ "palette_strays_stripped": [
+ "Taupe"
+ ],
+ "wrong_facets_stripped": [
+ "color:DARK PETROL MULTI"
+ ],
+ "tags_added": [
+ "color:Dark Petrol Multi"
+ ],
+ "tags_removed": [
+ "color:DARK PETROL MULTI",
+ "Taupe"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwrw-74510",
+ "vendor": "Rebel Walls",
+ "title": "Winding Spring - Black | Rebel Walls",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Charcoal"
+ ],
+ "parsed_colorway": "Black",
+ "new_color_tag": "color:Black",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Black",
+ "palette_strays_stripped": [
+ "Charcoal"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Black"
+ ],
+ "tags_removed": [
+ "Charcoal"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwrw-75543",
+ "vendor": "Rebel Walls",
+ "title": "Leo - King of the Circus | Rebel Walls",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Silver",
+ "White"
+ ],
+ "parsed_colorway": "King Of The Circus",
+ "new_color_tag": "color:King Of The Circus",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "King Of The Circus",
+ "palette_strays_stripped": [
+ "Silver",
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:King Of The Circus"
+ ],
+ "tags_removed": [
+ "Silver",
+ "White"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwrw-74468",
+ "vendor": "Rebel Walls",
+ "title": "Lily Pond - Rainbow | Rebel Walls",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Sage",
+ "White"
+ ],
+ "parsed_colorway": "Rainbow",
+ "new_color_tag": "color:Rainbow",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Rainbow",
+ "palette_strays_stripped": [
+ "Sage",
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Rainbow"
+ ],
+ "tags_removed": [
+ "Sage",
+ "White"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "ac2-4x14-maya-50037",
+ "vendor": "Maya Romanoff",
+ "title": "Grand Scale Ajiro Chevron - Bamboo | Maya Romanoff",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Beige",
+ "Sand"
+ ],
+ "parsed_colorway": "Bamboo",
+ "new_color_tag": "color:Bamboo",
+ "old_metafield_real_color_name": "Bamboo",
+ "new_metafield_real_color_name": "Bamboo",
+ "palette_strays_stripped": [
+ "Beige",
+ "Sand"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Bamboo"
+ ],
+ "tags_removed": [
+ "Beige",
+ "Sand"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "w-78-1389-s-maya-50234",
+ "vendor": "Maya Romanoff",
+ "title": "Conduit - Tempered Tan | Maya Romanoff",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Oatmeal"
+ ],
+ "parsed_colorway": "Tempered Tan",
+ "new_color_tag": "color:Tempered Tan",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Tempered Tan",
+ "palette_strays_stripped": [
+ "Oatmeal"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Tempered Tan"
+ ],
+ "tags_removed": [
+ "Oatmeal"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "obscure",
+ "vendor": "Mind the Gap",
+ "title": "Obscure - Beige Wallcovering | Mind the Gap",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Black",
+ "Light Gray"
+ ],
+ "parsed_colorway": "Beige",
+ "new_color_tag": "color:Beige",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Beige",
+ "palette_strays_stripped": [
+ "Black",
+ "Light Gray"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Beige"
+ ],
+ "tags_removed": [
+ "Black",
+ "Light Gray"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "floral-painting",
+ "vendor": "Mind the Gap",
+ "title": "Floral Painting - Beige Wallcovering | Mind the Gap",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Black"
+ ],
+ "parsed_colorway": "Beige",
+ "new_color_tag": "color:Beige",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Beige",
+ "palette_strays_stripped": [
+ "Black"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Beige"
+ ],
+ "tags_removed": [
+ "Black"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "raya-silk-embossed-caper-romo",
+ "vendor": "Romo",
+ "title": "Raya Silk Embossed - Caper Natural Wallcovering | Romo",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Taupe"
+ ],
+ "parsed_colorway": "Caper Natural",
+ "new_color_tag": "color:Caper Natural",
+ "old_metafield_real_color_name": "Caper",
+ "new_metafield_real_color_name": "Caper Natural",
+ "palette_strays_stripped": [
+ "Taupe"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Caper Natural"
+ ],
+ "tags_removed": [
+ "Taupe"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "monazite-cocktail-romo",
+ "vendor": "Romo",
+ "title": "Monazite - Cocktail Wallcovering | Romo",
+ "form": "pipe/dash",
+ "old_color_related_tags": [
+ "Taupe"
+ ],
+ "parsed_colorway": "Cocktail",
+ "new_color_tag": "color:Cocktail",
+ "old_metafield_real_color_name": "Cocktail",
+ "new_metafield_real_color_name": "Cocktail",
+ "palette_strays_stripped": [
+ "Taupe"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Cocktail"
+ ],
+ "tags_removed": [
+ "Taupe"
+ ],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwjp-10789-philip-jeffries-star-dust-silk-supernova-white-3224",
+ "vendor": "Phillip Jeffries",
+ "title": "Star Dust Silk - Supernova White",
+ "form": "bare/dash",
+ "old_color_related_tags": [],
+ "parsed_colorway": "Supernova White",
+ "new_color_tag": "color:Supernova White",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Supernova White",
+ "palette_strays_stripped": [],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Supernova White"
+ ],
+ "tags_removed": [],
+ "decision": "WRITE"
+ },
+ {
+ "handle": "dwjp-10726-philip-jeffries-cabana-weave-thundercloud-3108",
+ "vendor": "Phillip Jeffries",
+ "title": "Cabana Weave - Thundercloud",
+ "form": "bare/dash",
+ "old_color_related_tags": [
+ "White"
+ ],
+ "parsed_colorway": "Thundercloud",
+ "new_color_tag": "color:Thundercloud",
+ "old_metafield_real_color_name": "(empty)",
+ "new_metafield_real_color_name": "Thundercloud",
+ "palette_strays_stripped": [
+ "White"
+ ],
+ "wrong_facets_stripped": [],
+ "tags_added": [
+ "color:Thundercloud"
+ ],
+ "tags_removed": [
+ "White"
+ ],
+ "decision": "WRITE"
+ }
+ ],
+ "skips": [
+ {
+ "handle": "dwrw-76567",
+ "vendor": "Rebel Walls",
+ "title": "Arlon Teal | Rebel Walls",
+ "reason": "no-delimiter",
+ "form": "pipe/no-delim"
+ },
+ {
+ "handle": "dwrw-74241",
+ "vendor": "Rebel Walls",
+ "title": "Patinated Diamonds | Rebel Walls",
+ "reason": "no-delimiter",
+ "form": "pipe/no-delim"
+ },
+ {
+ "handle": "dwrw-76194",
+ "vendor": "Rebel Walls",
+ "title": "Memory Lane | Rebel Walls",
+ "reason": "no-delimiter",
+ "form": "pipe/no-delim"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/colorway-title-anchor/out/tk11076-nonchromatic-tails.json b/scripts/colorway-title-anchor/out/tk11076-nonchromatic-tails.json
new file mode 100644
index 0000000..99b2f0f
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/tk11076-nonchromatic-tails.json
@@ -0,0 +1,32 @@
+{
+ "write": 9155,
+ "chromatic": 3070,
+ "nonchromatic": 6085,
+ "examples": [
+ "Phillipe Romano: Distressed - Vineyard Polyurethane | Phillipe Romano => Vineyard Polyurethane",
+ "Phillipe Romano: Elevate - Hula Tekloom | Phillipe Romano => Hula Tekloom",
+ "Phillipe Romano: Elevate - Rust Tekloom | Phillipe Romano => Rust Tekloom",
+ "Rebel Walls: Happy Hills, Vanilla | Rebel Walls => Vanilla",
+ "Phillipe Romano: Mozart - Ink Blackout Fabric | Phillipe Romano => Ink Blackout Fabric",
+ "Phillipe Romano: Muse - Ridge Silicone Hybrid | Phillipe Romano Wallcoverings => Ridge Silicone Hybrid",
+ "Surface Stick: Elegante with Surface Stick - Faux Fine Wood Self Adhesive => Faux Fine Wood Self Adhesive",
+ "Designer Wallcoverings: Lawson Bay Lattice Trellis - Spice Orange => Spice Orange",
+ "Phillipe Romano: Pinpoint - Clay Vegan Leather | Phillipe Romano => Clay Vegan Leather",
+ "Phillipe Romano: Pinpoint - Merlot Vegan Leather | Phillipe Romano => Merlot Vegan Leather",
+ "Phillipe Romano: Pinpoint - Parsley Vegan Leather | Phillipe Romano => Parsley Vegan Leather",
+ "Phillipe Romano: Pinpoint - Perth Vegan Leather | Phillipe Romano => Perth Vegan Leather",
+ "Phillipe Romano: Pinpoint - Sea Green Vegan Leather | Phillipe Romano => Sea Green Vegan Leather",
+ "Phillipe Romano: Pinpoint - Sunset Vegan Leather | Phillipe Romano => Sunset Vegan Leather",
+ "Phillipe Romano: Polido - Hunter Vegan Leather | Phillipe Romano => Hunter Vegan Leather",
+ "Phillipe Romano: Polido - Smoke Vegan Leather | Phillipe Romano => Smoke Vegan Leather",
+ "Phillipe Romano: Polido - Trail Vegan Leather | Phillipe Romano => Trail Vegan Leather",
+ "Phillipe Romano: Prado - Bark Vegan Leather | Phillipe Romano => Bark Vegan Leather",
+ "Phillipe Romano: Prado - Black Vegan Leather | Phillipe Romano => Black Vegan Leather",
+ "Phillipe Romano: Prado - Husk Vegan Leather | Phillipe Romano => Husk Vegan Leather",
+ "Phillipe Romano: Prado - Oxblood Vegan Leather | Phillipe Romano => Oxblood Vegan Leather",
+ "Phillipe Romano: Prado - Walnut Vegan Leather | Phillipe Romano => Walnut Vegan Leather",
+ "Phillipe Romano: Prairie - Marine Breathable Vegan Leather | Phillipe Romano => Marine Breathable Vegan Leather",
+ "Phillipe Romano: Prime - Aubergiine Silicone Hybrid | Phillipe Romano => Aubergiine Silicone Hybrid",
+ "Phillipe Romano: Prime - Bark Silicone Hybrid | Phillipe Romano => Bark Silicone Hybrid"
+ ]
+}
\ No newline at end of file
diff --git a/scripts/colorway-title-anchor/parse-colorway.mjs b/scripts/colorway-title-anchor/parse-colorway.mjs
new file mode 100644
index 0000000..01cbee5
--- /dev/null
+++ b/scripts/colorway-title-anchor/parse-colorway.mjs
@@ -0,0 +1,173 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 — REUSABLE manufacturer-colorway parser (title-anchored, fleet-wide).
+ *
+ * Steve's LOCKED spec: TITLES ARE UNTOUCHED. We READ the live product title and parse the
+ * manufacturer COLORWAY out of it. The colorway is written to a structured metafield and a
+ * color:<colorway> tag; only bare AI-palette color-noise tags are stripped. Where the colorway
+ * cannot be cleanly isolated (or the pattern token itself is a color word) we SKIP — never guess.
+ *
+ * Two title grammars, both anchored on the SAME colorway delimiter:
+ * DASH-FORM (Kravet family): "<Pattern> - <Colorway> By <Vendor> [| Collection | Type…]"
+ * e.g. "Egerton - Indigo By Cole & Son", "Chandigarh - Sapphire Blue By Brunschwig & Fils | Volume 56"
+ * PIPE-FORM (Cole & Son, Maya Romanoff, Mind the Gap, Romo, …): "<Pattern> - <Colorway> | <Vendor>"
+ * e.g. "Florencecourt - Charcoal | Cole & Son", "Craze - Styled Sage | Maya Romanoff"
+ * COMMA-FORM (Rebel Walls, …): "<Pattern>, <Colorway> | <Vendor>"
+ * e.g. "Botanical Bliss - Rust | Rebel Walls", "Furada, Sage | Rebel Walls"
+ *
+ * ALGORITHM (deterministic, no AI):
+ * 1. Strip the VENDOR tail: prefer " By <Vendor>" (case-insensitive) if present; else drop
+ * everything from the FIRST top-level " | " onward (that segment is Vendor / Collection / Type).
+ * 2. From the remaining PATTERN+COLORWAY left-side, isolate the colorway as the text after the
+ * LAST colorway delimiter — " - " (or unicode dashes) OR ", " — whichever occurs last.
+ * (Using the LAST delimiter safely handles pattern names that contain an earlier hyphen/comma.)
+ * 3. Clean the colorway: drop a trailing "Wallcovering(s)"/"Wallpaper(s)" noise word, trim.
+ * 4. CONFIDENCE GATE → return {colorway:null, skip:true, reason} when:
+ * - no colorway delimiter present (pattern & colorway run together, e.g. "Aquila Rust") — cannot
+ * isolate without guessing; reason='no-delimiter'
+ * - the isolated colorway is empty / a placeholder ("Colorway 03") / absurdly long; reason='empty'|'placeholder'|'too-long'
+ * - the isolated colorway equals the whole left-side (delimiter was the vendor separator only); reason='no-pattern-split'
+ * - the PATTERN side is EMPTY (delimiter at very start); reason='no-pattern'
+ * NOTE: per Steve, we KEEP the mfr's literal colorway verbatim (incl. doubled words like "Green Green",
+ * slash-joined "Buff/Gold", multi-word "Sapphire Blue") — we do NOT bucket to a generic hue.
+ *
+ * The colorway may itself CONTAIN a color word (that is the whole point — "Indigo", "Sapphire Blue").
+ * Rule-6's "pattern token itself contains a color word → SKIP" only bites the no-delimiter case, which
+ * we already SKIP: without a delimiter we can't tell whether a color word belongs to the pattern
+ * ("Blue Willow") or is the colorway, so we never fabricate a split.
+ */
+
+const DASHES = /\s+[-–—]\s+/; // " - ", " – ", " — "
+const VENDOR_BY = /\s+By\s+.+$/i; // " By <Vendor> …" (Kravet family)
+const decode = s => String(s || '')
+ .replace(/&/g, '&').replace(/&/g, '&').replace(/'/g, "'")
+ .replace(/"/g, '"').replace(/"/g, '"').replace(/ /g, ' ');
+const collapse = s => decode(s).replace(/\s+/g, ' ').trim();
+// Unicode-aware Title Case: uppercase the first letter of each whitespace/slash/&/,/- run,
+// WITHOUT touching intra-word accented letters (fixes "Andalucía" -> was "AndalucíA").
+export const titleCase = s => collapse(s).replace(/(^|[\s/&,\-–—])([\p{L}])/gu, (_, sep, ch) => sep + ch.toUpperCase());
+
+const PLACEHOLDER = /^colorway\b/i;
+// Trailing marketing/noise phrases that are NEVER part of a colorway. Stripped from the FULL left-side
+// BEFORE the pattern|colorway split so "Ranger - Flax Vegan Leather - New Colors" splits at "- Flax…".
+const TRAILING_MARKETING = /\s*[-–—]\s*(new colors?|new colorways?|new|limited stock|limited edition|discontinued)\s*$/i;
+const TRAILING_NOISE = /\s+(wall\s*coverings?|wall\s*papers?)\s*$/i;
+
+// MATERIAL / SUBSTRATE suffixes the DW title format glues onto the colorway
+// (e.g. "Clay Vegan Leather", "Ink Blackout Fabric", "Marine Breathable Vegan Leather").
+// The real mfr colorway is the leading token(s); these trailing material words are NOT the colorway.
+// Ordered longest-first; matched only as a trailing suffix, and only if a non-empty colorway remains.
+const MATERIAL_SUFFIX = new RegExp(
+ '\\s+(' + [
+ 'breathable vegan leather', 'vegan leather', 'silicone hybrid', 'blackout fabric',
+ 'self adhesive', 'faux fine wood', 'faux wood grain', 'polyurethane', 'tekloom',
+ 'graffiti-free vinyl', 'graffiti free vinyl', 'grasscloth', 'durable vinyl', 'silk',
+ 'mylar', 'cork', 'sisal', 'raffia', 'jute', 'fabric', 'vinyl', 'wallcovering', 'wallpaper',
+ ].join('|') + ')\\s*$', 'i');
+
+/**
+ * Parse the manufacturer colorway from a live product title.
+ * @param {string} title the LIVE Shopify product title (never modified)
+ * @param {string} [vendor] the product's vendor (optional; used only to strip a trailing " | <Vendor>")
+ * @returns {{colorway: string|null, form: string, skip: boolean, reason: string|null, pattern: string|null}}
+ */
+export function parseColorway(title, vendor) {
+ const raw = collapse(title);
+ if (!raw) return { colorway: null, form: 'none', skip: true, reason: 'empty-title', pattern: null };
+
+ // 1. Strip vendor tail.
+ let form, left;
+ if (VENDOR_BY.test(raw)) {
+ form = 'dash-by';
+ left = raw.replace(VENDOR_BY, '').trim(); // "<Pattern> - <Colorway>"
+ } else if (raw.includes(' | ')) {
+ form = 'pipe';
+ left = raw.split(' | ')[0].trim(); // first top-level segment = "<Pattern> - <Colorway>"
+ } else {
+ form = 'bare';
+ left = raw.trim(); // no vendor delimiter at all
+ }
+ left = left.replace(TRAILING_NOISE, '').trim();
+ left = left.replace(TRAILING_MARKETING, '').trim(); // drop "- New Colors" etc. before the split
+
+ if (!left) return { colorway: null, form, skip: true, reason: 'empty-after-vendor-strip', pattern: null };
+
+ // 2. Split PATTERN | COLORWAY.
+ // DASH is the authoritative mfr delimiter → prefer it. Use the LAST " - " so a hyphenated
+ // pattern name splits correctly, and keep EVERYTHING after it (colorways can contain commas,
+ // e.g. "Black, Yellow, Red, Burgundy" is one multi-color colorway — do NOT split on those commas).
+ // Only when there is NO dash do we treat a COMMA as the delimiter (Rebel Walls "Pattern, Colorway"),
+ // and then take everything after the FIRST comma so a multi-color comma-list stays whole.
+ let idx = -1, delimLen = 0, delim = null;
+ const dashMatches = [...left.matchAll(/\s+[-–—]\s+/g)];
+ if (dashMatches.length) {
+ const m = dashMatches[dashMatches.length - 1];
+ idx = m.index; delimLen = m[0].length; delim = 'dash';
+ } else {
+ const m = left.match(/,\s+/); // FIRST comma
+ if (m) { idx = m.index; delimLen = m[0].length; delim = 'comma'; }
+ }
+
+ if (idx < 0) {
+ // No delimiter → pattern & colorway are fused (e.g. "Aquila Rust"). Cannot isolate → SKIP.
+ return { colorway: null, form: form + '/no-delim', skip: true, reason: 'no-delimiter', pattern: left };
+ }
+
+ const pattern = left.slice(0, idx).trim();
+ let colorway = left.slice(idx + delimLen).trim().replace(TRAILING_NOISE, '').trim();
+ // Strip a trailing material/substrate suffix ONLY if a non-empty colorway remains
+ // ("Clay Vegan Leather" -> "Clay"; but "Vegan Leather" alone would strip to '' -> keep as-is).
+ const stripped = colorway.replace(MATERIAL_SUFFIX, '').trim();
+ if (stripped) colorway = stripped;
+ form = form + '/' + delim;
+
+ // 3/4. Confidence gate.
+ if (!pattern) return { colorway: null, form, skip: true, reason: 'no-pattern', pattern: null };
+ if (!colorway) return { colorway: null, form, skip: true, reason: 'empty', pattern };
+ if (PLACEHOLDER.test(colorway)) return { colorway: null, form, skip: true, reason: 'placeholder', pattern };
+ if (colorway.length > 60) return { colorway: null, form, skip: true, reason: 'too-long', pattern };
+
+ return { colorway: titleCase(colorway), form, skip: false, reason: null, pattern: titleCase(pattern) };
+}
+
+// -------- self-test (node parse-colorway.mjs --selftest) --------
+if (process.argv[1] && process.argv[1].endsWith('parse-colorway.mjs') && process.argv.includes('--selftest')) {
+ const cases = [
+ ['Egerton - Indigo By Cole & Son', 'Cole & Son', 'Indigo', false],
+ ['Chandigarh - Sapphire Blue By Brunschwig & Fils | Volume 56 Print', 'Brunschwig & Fils', 'Sapphire Blue', false],
+ ['Faux Grass Texture On Vinyl - Sandstone Beige By Brunschwig & Fils | T', 'Brunschwig & Fils', 'Sandstone Beige', false],
+ ['Bird And Thistle - Green Green By Brunschwig & Fils | Toile', 'Brunschwig & Fils', 'Green Green', false],
+ ['Woods & Stars - Buff/Gold | Cole & Son', 'Cole & Son', 'Buff/Gold', false],
+ ['Florencecourt - Charcoal | Cole & Son', 'Cole & Son', 'Charcoal', false],
+ ['Craze - Styled Sage | Maya Romanoff', 'Maya Romanoff', 'Styled Sage', false],
+ ['Ajiro Fanfare Silver Birch Luster - Beige Wallcovering | Maya Romanoff', 'Maya Romanoff', 'Beige', false],
+ ['Botanical Bliss - Rust | Rebel Walls', 'Rebel Walls', 'Rust', false],
+ ['Furada, Sage | Rebel Walls', 'Rebel Walls', 'Sage', false],
+ ['Fish Shoal, Night Wallcoverings | Rebel Walls', 'Rebel Walls', 'Night', false],
+ ['Zebra Skin, Candy | Rebel Walls', 'Rebel Walls', 'Candy', false],
+ ['Aquila Rust | Rebel Walls', 'Rebel Walls', null, true], // no-delimiter → SKIP
+ ['Pom Pom | Rebel Walls', 'Rebel Walls', null, true], // no colorway → SKIP
+ ['Whimsy Wallcovering', 'Traditional Wh', null, true], // bare, no delim → SKIP
+ ['Grasscloth Natural | Phillipe Romano', 'Phillipe Romano', null, true], // no delim, fused → SKIP
+ ['TightWiki Grasscloth Deep Wedgewood Blue | Phillipe Romano', 'Phillipe Romano', null, true], // no delim → SKIP
+ ['Rayure Lione - Ciel Light Blue By Brunschwig & Fils | Madeleine Castaing', 'Brunschwig & Fils', 'Ciel Light Blue', false],
+ ['Sevilla Weaves - Andalucía Grey', 'Phillip Jeffries', 'Andalucía Grey', false], // accent preserved
+ ['Aristocracy - Black, Yellow, Red, Burgundy Wallcovering | Mind the Gap', 'Mind the Gap', 'Black, Yellow, Red, Burgundy', false], // dash split, comma-list kept whole
+ ['Furada, Sage | Rebel Walls', 'Rebel Walls', 'Sage', false], // comma-form single
+ ['Pinpoint - Clay Vegan Leather | Phillipe Romano', 'Phillipe Romano', 'Clay', false], // material suffix stripped
+ ['Prado - Oxblood Vegan Leather | Phillipe Romano', 'Phillipe Romano', 'Oxblood', false],
+ ['Mozart - Ink Blackout Fabric | Phillipe Romano', 'Phillipe Romano', 'Ink', false],
+ ['Distressed - Vineyard Polyurethane | Phillipe Romano', 'Phillipe Romano', 'Vineyard', false],
+ ['Umaji - Ivory By Brunschwig & Fils | Solid Wallcovering Grasscloth', 'Brunschwig & Fils', 'Ivory', false], // suffix only in the vendor tail, not colorway
+ ['Ranger - Flax Vegan Leather - New Colors | Phillipe Romano', 'Phillipe Romano', 'Flax', false], // "- New Colors" marketing tail dropped
+ ['Regent - Pepper Graffiti-Free Vinyl | Phillipe Romano', 'Phillipe Romano', 'Pepper', false],
+ ];
+ let pass = 0, fail = 0;
+ for (const [t, v, expCw, expSkip] of cases) {
+ const r = parseColorway(t, v);
+ const ok = (r.skip === expSkip) && ((r.colorway || null) === (expCw || null));
+ if (ok) pass++; else { fail++; console.log(`FAIL: ${JSON.stringify(t)}\n got colorway=${JSON.stringify(r.colorway)} skip=${r.skip} reason=${r.reason} form=${r.form}\n want colorway=${JSON.stringify(expCw)} skip=${expSkip}`); }
+ }
+ console.log(`\nself-test: ${pass}/${pass + fail} passed`);
+ process.exit(fail ? 1 : 0);
+}
diff --git a/scripts/colorway-title-anchor/tk11076-colorway-retag.mjs b/scripts/colorway-title-anchor/tk11076-colorway-retag.mjs
new file mode 100644
index 0000000..b2d9bb3
--- /dev/null
+++ b/scripts/colorway-title-anchor/tk11076-colorway-retag.mjs
@@ -0,0 +1,304 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 — FLEET colorway-correction driver (title-anchored). Steve's LOCKED spec.
+ *
+ * For each affected LIVE product (status ACTIVE, carrying AI-Analyzed-v2):
+ * 1. TITLE UNTOUCHED — parse the mfr colorway from the live title (parse-colorway.mjs, both grammars).
+ * 2. Write the parsed colorway to metafield custom.real_color_name (single_line_text_field) —
+ * the established store field for the real (non-AI) mfr colorway. NOT invented; NOT an AI field.
+ * 3. Set a color:<mfr colorway> tag (the REAL mfr colorway value, verbatim — not a hue bucket).
+ * 4. STRIP only the wrong AI palette-noise tags: (a) any existing color:* facet (AI-guessed,
+ * rebuilt from the title anchor); (b) bare color-word tags that are in THIS product's OWN
+ * AI palette (custom.color_details[].name + custom.color_{1,2,3}_name + custom.background_color +
+ * custom.color) AND are NOT part of the real colorway. Source-field driven, per-product —
+ * NEVER a global color denylist. PRESERVE every non-color tag and the real colorway.
+ * 5. CONFIDENCE GATE: parseColorway returns skip=true → leave the product AS-IS (no tag, no metafield,
+ * no strip). Tracked in the skips report.
+ * 6. NEVER change variant SKUs, price, status, sales channels, or images. Hue buckets OUT OF SCOPE.
+ *
+ * PostgreSQL-BEFORE-Shopify: the colorway anchor lives in the TITLE (already on Shopify) so there is
+ * no staging-table write needed; we record the old tags + old metafield in this VENDOR'S OWN rollback
+ * file (never a shared file — a shared file got clobbered by a concurrent race in a prior run), then
+ * write Shopify (authoritative), then per-item verify. Reversible + ledgered.
+ *
+ * node tk11076-colorway-retag.mjs --vendor="Cole & Son" # DRY-RUN report (no writes)
+ * node tk11076-colorway-retag.mjs --vendor="Cole & Son" --canary=8 # DRY-RUN, print N-product before/after diff
+ * node tk11076-colorway-retag.mjs --vendor="Cole & Son" --apply # LIVE (gated — do NOT run this pass)
+ * node tk11076-colorway-retag.mjs --canary-mixed # DRY-RUN diverse cross-vendor canary
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execSync } from 'node:child_process';
+import { parseColorway, titleCase } from './parse-colorway.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const OUT = path.join(HERE, 'out');
+fs.mkdirSync(OUT, { recursive: true });
+const DB = 'postgresql:///dw_unified?host=/tmp';
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const API = process.env.SHOPIFY_API_VERSION || '2024-10';
+const APPLY = process.argv.includes('--apply');
+const arg = k => (process.argv.find(a => a.startsWith(`--${k}=`)) || '').split('=').slice(1).join('=');
+const VENDOR = arg('vendor');
+const CANARY = parseInt(arg('canary') || '0', 10);
+const CANARY_MIXED = process.argv.includes('--canary-mixed');
+const CW_META_NS = 'custom', CW_META_KEY = 'real_color_name';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const decode = s => String(s || '').replace(/&/g, '&').replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"');
+const norm = s => decode(s).toLowerCase().replace(/\s*&\s*/g, ' & ').replace(/\s*\/\s*/g, '/').replace(/\s+/g, ' ').trim();
+
+// Color lexicon — GUARD ONLY. A tag qualifies as a palette-noise stray candidate only if every
+// space/slash/&-joined token is a known color word/modifier. The STRIP DECISION is palette membership
+// (this product's OWN AI palette), not this list. This just prevents mistaking a non-color tag.
+const COLOR_MODIFIERS = new Set(['soft','pale','dark','light','deep','bright','warm','cool','dusty',
+ 'metallic','metalic','antique','burnished','muted','rich','pastel','vintage','&','and','on','multi']);
+const COLOR_WORDS = new Set([
+ 'white','black','grey','gray','ivory','cream','beige','stone','dove','taupe','greige','oatmeal',
+ 'alabaster','bone','eggshell','linen','sand','almond','pearl','chalk','parchment','champagne','ecru',
+ 'silver','gold','gilver','bronze','copper','pewter','platinum','mica','graphite','charcoal','slate','pebble',
+ 'teal','aqua','turquoise','jade','celadon','sage','mint','emerald','forest','olive','chartreuse','lime','green',
+ 'blue','navy','indigo','cobalt','sapphire','cornflower','sky','denim','powder','peacock','azure','petrol','cyan',
+ 'red','crimson','scarlet','burgundy','wine','russet','terracotta','rust','brick','coral','salmon','blush','cerise',
+ 'pink','rose','fuchsia','magenta','lavender','lilac','purple','plum','amethyst','mauve','violet','orchid',
+ 'yellow','ochre','saffron','mustard','honey','amber','peach','apricot','tan','walnut','espresso','coffee','caramel',
+ 'brown','chocolate','khaki','ecru','natural','neutral','midnight','seafoam','ash','buff','fawn','sepia','clay',
+ 'orange','tangerine','rust','marigold','flax','wheat','straw','camel','mahogany','cocoa','mocha','stone','smoke']);
+function isColorish(tag) {
+ const t = norm(tag);
+ if (!t) return false;
+ return t.split(/[\s/&]+/).filter(Boolean).every(p => COLOR_WORDS.has(p) || COLOR_MODIFIERS.has(p));
+}
+
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+// Query handles safely (SQL via temp file — no shell interpolation of vendor names / $$).
+function psqlHandles(vendor, extraWhere, limit) {
+ const sql = `SELECT handle FROM shopify_products
+ WHERE vendor = '${vendor.replace(/'/g, "''")}' AND status='ACTIVE'
+ AND tags ILIKE '%AI-Analyzed-v2%' AND ${extraWhere}
+ ORDER BY random() LIMIT ${parseInt(limit, 10)}`;
+ const tmp = path.join(OUT, `.q-${Date.now()}-${Math.random().toString(36).slice(2)}.sql`);
+ fs.writeFileSync(tmp, sql);
+ try { return execSync(`psql "${DB}" -tAf "${tmp}"`, { encoding: 'utf8' }).trim().split('\n').map(s => s.trim()).filter(Boolean); }
+ finally { fs.unlinkSync(tmp); }
+}
+
+async function gql(query, variables) {
+ for (let a = 0; a < 6; a++) {
+ const res = await fetch(`https://${STORE}/admin/api/${API}/graphql.json`, {
+ method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }), signal: AbortSignal.timeout(45000) }).catch(e => ({ _err: e }));
+ if (res._err) { await sleep(1500 * (a + 1)); continue; }
+ const j = await res.json().catch(() => ({}));
+ if (j.errors) { if (a === 5) return { errors: j.errors }; await sleep(1500 * (a + 1)); continue; }
+ await sleep(350); return j.data;
+ }
+ return { errors: [{ message: 'gql retries exhausted' }] };
+}
+
+const PROD_FIELDS = `id handle status title vendor tags
+ cw: metafield(namespace:"${CW_META_NS}", key:"${CW_META_KEY}"){ value }
+ cd: metafield(namespace:"custom", key:"color_details"){ value }
+ c1: metafield(namespace:"custom", key:"color_1_name"){ value }
+ c2: metafield(namespace:"custom", key:"color_2_name"){ value }
+ c3: metafield(namespace:"custom", key:"color_3_name"){ value }
+ cc: metafield(namespace:"custom", key:"color"){ value }
+ bg: metafield(namespace:"custom", key:"background_color"){ value }
+ variants(first:3){ nodes{ sku } }`;
+
+async function fetchVendor(vendor) {
+ const out = []; let cursor = null;
+ const q = `query($cursor:String){ products(first:100, query:${JSON.stringify(`vendor:'${vendor}' AND tag:AI-Analyzed-v2 AND status:active`)}, after:$cursor){
+ pageInfo{ hasNextPage endCursor } nodes{ ${PROD_FIELDS} } } }`;
+ for (let g = 0; g < 80; g++) {
+ const d = await gql(q, { cursor });
+ if (!d || d.errors) { console.error('fetch err', JSON.stringify(d && d.errors)); break; }
+ out.push(...d.products.nodes);
+ if (!d.products.pageInfo.hasNextPage) break;
+ cursor = d.products.pageInfo.endCursor;
+ }
+ return out;
+}
+
+async function fetchHandles(handles) {
+ const out = [];
+ for (const h of handles) {
+ const d = await gql(`query($q:String!){ products(first:1, query:$q){ nodes{ ${PROD_FIELDS} } } }`, { q: `handle:${h}` });
+ if (d && d.products && d.products.nodes[0]) out.push(d.products.nodes[0]);
+ }
+ return out;
+}
+
+// Build the per-product plan (pure — no writes). Returns {plan|null, skip|null}.
+function planProduct(p) {
+ const parsed = parseColorway(p.title, p.vendor);
+ if (parsed.skip) {
+ return { skip: { handle: p.handle, vendor: p.vendor, title: p.title, reason: parsed.reason, form: parsed.form } };
+ }
+ const colorway = parsed.colorway; // real mfr colorway, verbatim (title case)
+ const facet = 'color:' + colorway;
+
+ // THIS product's OWN AI palette (source-field driven, per-product).
+ const palette = new Set();
+ try { for (const c of JSON.parse((p.cd && p.cd.value) || '[]')) { const n = c && (c.name || c.color); if (n) palette.add(norm(n)); } } catch {}
+ for (const v of [p.c1 && p.c1.value, p.c2 && p.c2.value, p.c3 && p.c3.value, p.bg && p.bg.value, p.cc && p.cc.value]) if (v) palette.add(norm(v));
+
+ const colorwayTokens = new Set(norm(colorway).split(/[\s/&,]+/).filter(Boolean));
+ const oldTags = Array.isArray(p.tags) ? p.tags.slice() : String(p.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+
+ const newTags = [];
+ const strippedFacet = [], strippedPalette = [];
+ for (const t of oldTags) {
+ if (/^color:/i.test(t)) { strippedFacet.push(t); continue; } // rebuild from title
+ // strip a bare AI-palette color word that is NOT part of the real colorway
+ const isPaletteStray = palette.has(norm(t)) && isColorish(t) && !colorwayTokens.has(norm(t)) && norm(t) !== norm(colorway);
+ if (isPaletteStray) { strippedPalette.push(t); continue; }
+ if (norm(t) === norm(facet)) continue; // dedup, re-added below
+ newTags.push(t); // PRESERVE all non-color tags
+ }
+ if (!newTags.some(t => norm(t) === norm(facet))) newTags.push(facet);
+
+ const oldCw = (p.cw && p.cw.value) || '';
+ const metafieldChange = norm(oldCw) !== norm(colorway);
+ const tagsChanged = JSON.stringify([...oldTags].map(norm).sort()) !== JSON.stringify([...newTags].map(norm).sort());
+ if (!metafieldChange && !tagsChanged) return { plan: null }; // already correct
+
+ return { plan: {
+ id: p.id, handle: p.handle, status: p.status, vendor: p.vendor, title: p.title,
+ colorway, form: parsed.form, oldTags, newTags, strippedFacet, strippedPalette,
+ oldColorwayMeta: oldCw, newColorwayMeta: colorway,
+ } };
+}
+
+function diffRow(pl) {
+ const add = pl.newTags.filter(t => !pl.oldTags.includes(t));
+ const rem = pl.oldTags.filter(t => !pl.newTags.includes(t));
+ const oldColorRelated = pl.oldTags.filter(t => /^color:/i.test(t) || pl.strippedPalette.includes(t));
+ return {
+ handle: pl.handle, vendor: pl.vendor, title: pl.title, form: pl.form,
+ old_color_related_tags: oldColorRelated,
+ parsed_colorway: pl.colorway,
+ new_color_tag: 'color:' + pl.colorway,
+ old_metafield_real_color_name: pl.oldColorwayMeta || '(empty)',
+ new_metafield_real_color_name: pl.newColorwayMeta,
+ palette_strays_stripped: pl.strippedPalette,
+ wrong_facets_stripped: pl.strippedFacet,
+ tags_added: add, tags_removed: rem,
+ decision: 'WRITE',
+ };
+}
+
+async function run() {
+ let live = [];
+ if (CANARY_MIXED) {
+ // diverse cross-vendor + Steve's named checks (Cole & Son Charcoal, Rebel Walls Rust)
+ const wanted = {
+ 'Clarke And Clarke': 2, 'Brunschwig & Fils': 3, 'Lee Jofa': 1, 'GP & J Baker': 1, 'Groundworks': 1,
+ 'Cole & Son': 3, 'Rebel Walls': 3, 'Maya Romanoff': 2, 'Mind the Gap': 2, 'Romo': 2, 'Phillip Jeffries': 2,
+ };
+ // pinned handles Steve called out (Cole & Son Florencecourt-Charcoal, Rebel Walls Botanical Bliss - Rust)
+ const pinned = ['botanical-bliss-rust-wallcoverings-dwrw-360566'];
+ const pin = await fetchHandles(pinned);
+ live.push(...pin);
+ for (const [v, n] of Object.entries(wanted)) {
+ const rows = psqlHandles(v, "title LIKE '% - %'", n * 3);
+ const got = await fetchHandles(rows.slice(0, n * 3));
+ // keep the first n that produce a WRITE (non-skip) so the canary is informative, but also keep 1 skip if available
+ const writes = got.filter(p => !planProduct(p).skip).slice(0, n);
+ live.push(...writes);
+ }
+ // add a couple of deliberate SKIP cases to show the gate working
+ const skipRows = psqlHandles('Rebel Walls', "title NOT LIKE '% - %' AND title NOT LIKE '%,%'", 3);
+ live.push(...await fetchHandles(skipRows));
+ } else if (VENDOR) {
+ live = await fetchVendor(VENDOR);
+ if (CANARY) live = live.slice(0, CANARY * 4); // oversample; diff picks first CANARY writes below
+ } else {
+ console.error('specify --vendor="X" or --canary-mixed'); process.exit(1);
+ }
+
+ const plans = [], skips = [], diffRows = [];
+ for (const p of live) {
+ const r = planProduct(p);
+ if (r.skip) { skips.push(r.skip); continue; }
+ if (r.plan) { plans.push(r.plan); diffRows.push(diffRow(r.plan)); }
+ }
+
+ // canary diff cap
+ const shownDiff = CANARY ? diffRows.slice(0, CANARY) : diffRows;
+ const shownSkips = skips;
+
+ const report = {
+ ts: new Date().toISOString(),
+ mode: APPLY ? 'APPLY' : 'DRY-RUN',
+ scope: CANARY_MIXED ? 'canary-mixed' : VENDOR,
+ live_products_examined: live.length,
+ would_write: plans.length,
+ would_skip: skips.length,
+ metafield_target: `${CW_META_NS}.${CW_META_KEY}`,
+ before_after: shownDiff,
+ skips: shownSkips,
+ };
+ const outFile = path.join(OUT, CANARY_MIXED ? 'tk11076-canary-mixed-diff.json' : `tk11076-${(VENDOR || 'x').replace(/\W+/g, '_')}-diff.json`);
+ fs.writeFileSync(outFile, JSON.stringify(report, null, 2));
+
+ // console table
+ console.log(`\n=== TK-11076 colorway retag ${report.mode} · scope=${report.scope} ===`);
+ console.log(`examined ${live.length} · WRITE ${plans.length} · SKIP ${skips.length} · metafield=${report.metafield_target}`);
+ for (const d of shownDiff) {
+ console.log(`\n[${d.vendor}] "${d.title}" (${d.form})`);
+ console.log(` old color-tags : [${d.old_color_related_tags.join(', ')}]`);
+ console.log(` parsed colorway: ${d.parsed_colorway}`);
+ console.log(` new color: tag : ${d.new_color_tag}`);
+ console.log(` metafield real_color_name: ${d.old_metafield_real_color_name} -> ${d.new_metafield_real_color_name}`);
+ console.log(` palette strays stripped : [${d.palette_strays_stripped.join(', ')}]`);
+ console.log(` DECISION: WRITE`);
+ }
+ if (shownSkips.length) {
+ console.log(`\n--- SKIPS (confidence gate) ---`);
+ for (const s of shownSkips) console.log(` SKIP [${s.vendor}] "${s.title}" reason=${s.reason} (${s.form})`);
+ }
+ console.log(`\nfull diff JSON: ${outFile}`);
+
+ if (!APPLY) { console.log(`\nDRY-RUN — no Shopify or PG writes. (LIVE fleet write is gated; do not run --apply this pass.)`); return; }
+
+ // ---- LIVE APPLY (per-vendor rollback file; NOT reached this canary pass) ----
+ if (!VENDOR) { console.error('refuse --apply without a single --vendor (per-vendor rollback isolation)'); process.exit(1); }
+ const ROLLBACK = path.join(process.env.HOME, `.claude/yolo-queue/executed-reversible/TK-11076-${VENDOR.replace(/\W+/g, '_')}-rollback.json`);
+ const rollback = fs.existsSync(ROLLBACK) ? JSON.parse(fs.readFileSync(ROLLBACK, 'utf8')) : {};
+ let done = 0, pass = 0, fail = 0, batch = 0;
+ for (const pl of plans) {
+ rollback[pl.handle] = { id: pl.id, ts: new Date().toISOString(), old_tags: pl.oldTags, new_tags: pl.newTags,
+ old_metafield: pl.oldColorwayMeta, new_metafield: pl.newColorwayMeta };
+ fs.writeFileSync(ROLLBACK, JSON.stringify(rollback, null, 2));
+
+ const d = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id }
+ metafields:product{ id } userErrors{ field message } } }`,
+ { input: { id: pl.id, tags: pl.newTags,
+ metafields: [{ namespace: CW_META_NS, key: CW_META_KEY, type: 'single_line_text_field', value: pl.newColorwayMeta }] } });
+ const ue = d && d.productUpdate && d.productUpdate.userErrors;
+ if (ue && ue.length) { console.error(` userErrors ${pl.handle}: ${JSON.stringify(ue)}`); fail++; continue; }
+
+ const v = await gql(`query($id:ID!){ product(id:$id){ tags m:metafield(namespace:"${CW_META_NS}",key:"${CW_META_KEY}"){ value } } }`, { id: pl.id });
+ const lt = (v && v.product && v.product.tags) || [];
+ const facetOk = lt.some(t => norm(t) === norm('color:' + pl.colorway));
+ const metaOk = norm((v && v.product && v.product.m && v.product.m.value) || '') === norm(pl.colorway);
+ const ok = facetOk && metaOk;
+ ok ? pass++ : (fail++, console.error(` VERIFY FAIL ${pl.handle}: facetOk=${facetOk} metaOk=${metaOk}`));
+ done++; console.log(`[${done}/${plans.length}] ${pl.handle} ${ok ? '✓' : 'VERIFY-FAIL'}`);
+
+ try { execSync(`node "${path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/log-exec.mjs')}" ` +
+ `--agent vp-dw-commerce --ticket TK-11076 --action "colorway retag ${pl.handle}: color:${pl.colorway}" --blast 1 ` +
+ `--undo "cd ${HERE} && node tk11076-rollback.mjs --vendor=\\"${VENDOR}\\" --only=${pl.handle} --apply" ` +
+ `--verify "GET ${pl.id} — real_color_name + color: facet == title colorway; palette strays gone; title/price/status/sku/image unchanged"`,
+ { stdio: 'ignore' }); } catch {}
+
+ batch++;
+ if (batch % 25 === 0 && done < plans.length) { console.log(' …90s inter-batch gap…'); await sleep(90000); }
+ else await sleep(700);
+ }
+ console.log(`\nDONE ${VENDOR}: ${done} · verify-pass ${pass} · verify-fail ${fail} · rollback=${ROLLBACK}`);
+}
+run();
diff --git a/scripts/colorway-title-anchor/tk11076-rollback.mjs b/scripts/colorway-title-anchor/tk11076-rollback.mjs
new file mode 100644
index 0000000..e2d531b
--- /dev/null
+++ b/scripts/colorway-title-anchor/tk11076-rollback.mjs
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 rollback — restore old tags + old custom.real_color_name from a VENDOR'S OWN rollback file.
+ * Never touches titles/price/status/sku/image (the forward run never did either).
+ * node tk11076-rollback.mjs --vendor="Cole & Son" # DRY-RUN (list what would revert)
+ * node tk11076-rollback.mjs --vendor="Cole & Son" --apply # revert ALL for that vendor
+ * node tk11076-rollback.mjs --vendor="Cole & Son" --only=h1,h2 --apply
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const API = process.env.SHOPIFY_API_VERSION || '2024-10';
+const APPLY = process.argv.includes('--apply');
+const arg = k => (process.argv.find(a => a.startsWith(`--${k}=`)) || '').split('=').slice(1).join('=');
+const VENDOR = arg('vendor');
+const ONLY = (arg('only') || '').split(',').filter(Boolean);
+if (!TOKEN || !VENDOR) { console.error('need SHOPIFY_ADMIN_TOKEN + --vendor="X"'); process.exit(1); }
+const ROLLBACK = path.join(process.env.HOME, `.claude/yolo-queue/executed-reversible/TK-11076-${VENDOR.replace(/\W+/g, '_')}-rollback.json`);
+if (!fs.existsSync(ROLLBACK)) { console.error('no rollback file:', ROLLBACK); process.exit(1); }
+const map = JSON.parse(fs.readFileSync(ROLLBACK, 'utf8'));
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function gql(q, v) { const r = await fetch(`https://${STORE}/admin/api/${API}/graphql.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); return (await r.json()).data; }
+const entries = Object.entries(map).filter(([h]) => !ONLY.length || ONLY.includes(h));
+console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'} rollback ${VENDOR}: ${entries.length} products`);
+for (const [h, r] of entries) {
+ console.log(` ${h}: tags -> [${(r.old_tags || []).join(', ')}] real_color_name -> "${r.old_metafield || '(empty)'}"`);
+ if (!APPLY) continue;
+ await gql(`mutation($i:ProductInput!){ productUpdate(input:$i){ userErrors{ message } } }`,
+ { i: { id: r.id, tags: r.old_tags || [], metafields: [{ namespace: 'custom', key: 'real_color_name', type: 'single_line_text_field', value: r.old_metafield || '' }] } });
+ await sleep(500);
+}
+console.log(APPLY ? 'rollback applied.' : 'DRY-RUN — re-run with --apply to revert.');
← 3d0551a auto-data-snapshot: 2026-09-01T12:55:30 (10 data files) — sc
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-01T13:36:10 (3 data files) — scr 062be9a →