[object Object]

← back to Designerwallcoverings

TK-11076 China Seas retag: canary-first remediate+rollback scripts (STOPPED unsafe — color_name anchor unreliable vs title colorway)

ce61a404dfb387bbe73bbc9835ddc2703f716404 · 2026-09-01 12:37:12 -0700 · Steve Abrams

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

Files touched

Diff

commit ce61a404dfb387bbe73bbc9835ddc2703f716404
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 12:37:12 2026 -0700

    TK-11076 China Seas retag: canary-first remediate+rollback scripts (STOPPED unsafe — color_name anchor unreliable vs title colorway)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/china-seas-onboard/out/tk11076-canary.json | 637 +++++++++++++++++++++
 scripts/china-seas-onboard/out/tk11076-skips.json  |   7 +
 scripts/china-seas-onboard/tk11076-remediate.mjs   | 280 +++++++++
 scripts/china-seas-onboard/tk11076-rollback.mjs    |  50 ++
 4 files changed, 974 insertions(+)

diff --git a/scripts/china-seas-onboard/out/tk11076-canary.json b/scripts/china-seas-onboard/out/tk11076-canary.json
new file mode 100644
index 0000000..710ef17
--- /dev/null
+++ b/scripts/china-seas-onboard/out/tk11076-canary.json
@@ -0,0 +1,637 @@
+{
+  "picked": 6,
+  "would_strip_legit_colorway_token": 0,
+  "diff": [
+    {
+      "handle": "dwcs-885822",
+      "status": "ACTIVE",
+      "mfr": "885822",
+      "title": "Abaco Stripe Camel Beiges Screen Printed - Cream Fabric | China Seas",
+      "real_colorway": "Cream",
+      "facet_added": "color:Cream",
+      "before_tags": [
+        "100% linen fabric",
+        "11\" repeat",
+        "52\" wide",
+        "Abaco Stripe",
+        "Abaco Stripe Camel Beiges Screen Printed Fabric",
+        "Abstract",
+        "AI-Analyzed-v2",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Background Color Tan",
+        "Beige",
+        "Bird",
+        "China Seas Fabrics",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "Coral",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Linen",
+        "Needs-Price",
+        "Needs-Width",
+        "Pastel",
+        "Pattern",
+        "quotes",
+        "Sample",
+        "Showroom Line",
+        "Sold Per Unit",
+        "Stripe",
+        "Tan",
+        "Texture",
+        "Transitional",
+        "Wallcovering",
+        "White"
+      ],
+      "after_tags": [
+        "100% linen fabric",
+        "11\" repeat",
+        "52\" wide",
+        "Abaco Stripe",
+        "Abaco Stripe Camel Beiges Screen Printed Fabric",
+        "Abstract",
+        "AI-Analyzed-v2",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Bird",
+        "China Seas Fabrics",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Sample",
+        "Showroom Line",
+        "Sold Per Unit",
+        "Stripe",
+        "Texture",
+        "Transitional",
+        "Wallcovering",
+        "color:Cream",
+        "Cream"
+      ],
+      "stripped_palette": [
+        "Beige",
+        "Coral",
+        "Linen",
+        "Pastel",
+        "Tan",
+        "White"
+      ],
+      "stripped_bg": [
+        "Background Color Tan"
+      ],
+      "preserved_noncolor": [
+        "100% linen fabric",
+        "11\" repeat",
+        "52\" wide",
+        "Abaco Stripe",
+        "Abaco Stripe Camel Beiges Screen Printed Fabric",
+        "Abstract",
+        "AI-Analyzed-v2",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Bird",
+        "China Seas Fabrics",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Sample",
+        "Showroom Line",
+        "Sold Per Unit",
+        "Stripe",
+        "Texture",
+        "Transitional",
+        "Wallcovering"
+      ]
+    },
+    {
+      "handle": "dwcs-916522",
+      "status": "ACTIVE",
+      "mfr": "916522",
+      "title": "Fiorentina Two Color French Blue New Navy Screen Printed Fabric | China Seas",
+      "real_colorway": "Mauve",
+      "facet_added": "color:Mauve",
+      "before_tags": [
+        "4.5\" repeat",
+        "48\" wide",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Ash",
+        "Available in Suncloth for outdoor use",
+        "Background Color Light Blue",
+        "Blue",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Fiorentina Two Color",
+        "French",
+        "French Blue New Navy",
+        "Geometric",
+        "Lattice",
+        "Light Blue",
+        "Linen",
+        "Needs-Price",
+        "Needs-Width",
+        "New",
+        "Pattern",
+        "Periwinkle",
+        "quotes",
+        "Showroom Line",
+        "Smoke",
+        "Solid",
+        "Steel",
+        "Texture",
+        "Wallcovering",
+        "White"
+      ],
+      "after_tags": [
+        "4.5\" repeat",
+        "48\" wide",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Fiorentina Two Color",
+        "French",
+        "French Blue New Navy",
+        "Geometric",
+        "Lattice",
+        "Needs-Price",
+        "Needs-Width",
+        "New",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Smoke",
+        "Solid",
+        "Steel",
+        "Texture",
+        "Wallcovering",
+        "color:Mauve",
+        "Mauve"
+      ],
+      "stripped_palette": [
+        "Ash",
+        "Blue",
+        "Light Blue",
+        "Linen",
+        "Periwinkle",
+        "White"
+      ],
+      "stripped_bg": [
+        "Background Color Light Blue"
+      ],
+      "preserved_noncolor": [
+        "4.5\" repeat",
+        "48\" wide",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Fiorentina Two Color",
+        "French",
+        "French Blue New Navy",
+        "Geometric",
+        "Lattice",
+        "Needs-Price",
+        "Needs-Width",
+        "New",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Smoke",
+        "Solid",
+        "Steel",
+        "Texture",
+        "Wallcovering"
+      ]
+    },
+    {
+      "handle": "dwcs-951922",
+      "status": "ACTIVE",
+      "mfr": "951922",
+      "title": "Nairobi Petite Windsor Blue on Tint Screen Printed Fabric | China Seas",
+      "real_colorway": "Periwinkle",
+      "facet_added": "color:Periwinkle",
+      "before_tags": [
+        "100% linen fabric",
+        "52\" width / 18\" repeat",
+        "53\" width / 12\" repeat",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Animal",
+        "Animal Print",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Background Color Light Blue",
+        "Black on White",
+        "Blue",
+        "Chevron",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Light Blue",
+        "Linen",
+        "Nairobi Petite",
+        "Nairobi Petite Windsor Blue on Tint Screen Printed Fabric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pastel",
+        "Pattern",
+        "Powder Blue",
+        "quotes",
+        "Showroom Line",
+        "Solid",
+        "Texture",
+        "Wallcovering",
+        "White",
+        "Windsor Blue on Tint",
+        "Zebra"
+      ],
+      "after_tags": [
+        "100% linen fabric",
+        "52\" width / 18\" repeat",
+        "53\" width / 12\" repeat",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Animal",
+        "Animal Print",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Black on White",
+        "Chevron",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Nairobi Petite",
+        "Nairobi Petite Windsor Blue on Tint Screen Printed Fabric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Solid",
+        "Texture",
+        "Wallcovering",
+        "Windsor Blue on Tint",
+        "Zebra",
+        "color:Periwinkle",
+        "Periwinkle"
+      ],
+      "stripped_palette": [
+        "Blue",
+        "Light Blue",
+        "Linen",
+        "Pastel",
+        "Powder Blue",
+        "White"
+      ],
+      "stripped_bg": [
+        "Background Color Light Blue"
+      ],
+      "preserved_noncolor": [
+        "100% linen fabric",
+        "52\" width / 18\" repeat",
+        "53\" width / 12\" repeat",
+        "55% linen / 45% cotton fabric",
+        "AI-Analyzed-v2",
+        "Animal",
+        "Animal Print",
+        "Animal/Insects",
+        "Architectural",
+        "Available in Suncloth for outdoor use",
+        "Black on White",
+        "Chevron",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Coastal",
+        "Commercial",
+        "display_variant",
+        "Fabric",
+        "Nairobi Petite",
+        "Nairobi Petite Windsor Blue on Tint Screen Printed Fabric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Solid",
+        "Texture",
+        "Wallcovering",
+        "Windsor Blue on Tint",
+        "Zebra"
+      ]
+    },
+    {
+      "handle": "dwcs-983722",
+      "status": "ACTIVE",
+      "mfr": "983722",
+      "title": "Ziggurat Steel Steel on Tan Screen Printed - White Fabric | China Seas",
+      "real_colorway": "White",
+      "facet_added": "color:White",
+      "before_tags": [
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Beige",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "Contemporary",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Gray",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Steel on Tan",
+        "Tan",
+        "Wallcovering",
+        "White",
+        "Ziggurat Steel",
+        "Ziggurat Steel Steel on Tan Screen Printed Fabric"
+      ],
+      "after_tags": [
+        "AI-Analyzed-v2",
+        "Architectural",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "Contemporary",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Steel on Tan",
+        "Wallcovering",
+        "White",
+        "Ziggurat Steel",
+        "Ziggurat Steel Steel on Tan Screen Printed Fabric",
+        "color:White"
+      ],
+      "stripped_palette": [
+        "Beige",
+        "Gray",
+        "Tan"
+      ],
+      "stripped_bg": [],
+      "preserved_noncolor": [
+        "AI-Analyzed-v2",
+        "Architectural",
+        "China Seas Fabrics",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "Contemporary",
+        "display_variant",
+        "Fabric",
+        "Geometric",
+        "Needs-Price",
+        "Needs-Width",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Steel on Tan",
+        "Wallcovering",
+        "Ziggurat Steel",
+        "Ziggurat Steel Steel on Tan Screen Printed Fabric"
+      ]
+    },
+    {
+      "handle": "dwcw-730021",
+      "status": "ACTIVE",
+      "mfr": "730021",
+      "title": "Happy Garden Background Grey on White Screen Printed | China Seas",
+      "real_colorway": "Gray",
+      "facet_added": "color:Gray",
+      "before_tags": [
+        "27\" Width x 5 yds",
+        "41.5\"V Repeat",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Ash",
+        "Background Color Light Gray",
+        "Botanical",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "display_variant",
+        "Grey on White",
+        "Happy Garden Background",
+        "Happy Garden Background Grey on White Screen Printed",
+        "Light Gray",
+        "Needs-Price",
+        "Needs-Width",
+        "Organic Modern",
+        "Paper",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Silver",
+        "Wallcovering",
+        "White"
+      ],
+      "after_tags": [
+        "27\" Width x 5 yds",
+        "41.5\"V Repeat",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Botanical",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "display_variant",
+        "Grey on White",
+        "Happy Garden Background",
+        "Happy Garden Background Grey on White Screen Printed",
+        "Needs-Price",
+        "Needs-Width",
+        "Organic Modern",
+        "Paper",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Wallcovering",
+        "color:Gray",
+        "Gray"
+      ],
+      "stripped_palette": [
+        "Ash",
+        "Light Gray",
+        "Silver",
+        "White"
+      ],
+      "stripped_bg": [
+        "Background Color Light Gray"
+      ],
+      "preserved_noncolor": [
+        "27\" Width x 5 yds",
+        "41.5\"V Repeat",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Botanical",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "display_variant",
+        "Grey on White",
+        "Happy Garden Background",
+        "Happy Garden Background Grey on White Screen Printed",
+        "Needs-Price",
+        "Needs-Width",
+        "Organic Modern",
+        "Paper",
+        "Pattern",
+        "quotes",
+        "Showroom Line",
+        "Wallcovering"
+      ]
+    },
+    {
+      "handle": "dwcw-760221",
+      "status": "ACTIVE",
+      "mfr": "760221",
+      "title": "Persia Navy_on_almost_white Screen Printed - Cream Wallcovering | China Seas",
+      "real_colorway": "Cream",
+      "facet_added": "color:Cream",
+      "before_tags": [
+        "14\" Repeat",
+        "27\" Width x 5 yds",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Background Color Off-white",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "Cream",
+        "display_variant",
+        "Geometric",
+        "Navy Blue",
+        "Navy_on_Almost_White",
+        "Needs-Price",
+        "Needs-Width",
+        "Off-white",
+        "Paper",
+        "Pattern",
+        "Persia",
+        "Persia Navy_on_Almost_White Screen Printed",
+        "quotes",
+        "Showroom Line",
+        "Silver",
+        "Wallcovering",
+        "White"
+      ],
+      "after_tags": [
+        "14\" Repeat",
+        "27\" Width x 5 yds",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "Cream",
+        "display_variant",
+        "Geometric",
+        "Navy_on_Almost_White",
+        "Needs-Price",
+        "Needs-Width",
+        "Off-white",
+        "Paper",
+        "Pattern",
+        "Persia",
+        "Persia Navy_on_Almost_White Screen Printed",
+        "quotes",
+        "Showroom Line",
+        "Wallcovering",
+        "color:Cream"
+      ],
+      "stripped_palette": [
+        "Navy Blue",
+        "Silver",
+        "White"
+      ],
+      "stripped_bg": [
+        "Background Color Off-white"
+      ],
+      "preserved_noncolor": [
+        "14\" Repeat",
+        "27\" Width x 5 yds",
+        "AI-Analyzed-v2",
+        "Architectural",
+        "Chinoiserie",
+        "Class A Fire Rated",
+        "Commercial",
+        "display_variant",
+        "Geometric",
+        "Navy_on_Almost_White",
+        "Needs-Price",
+        "Needs-Width",
+        "Off-white",
+        "Paper",
+        "Pattern",
+        "Persia",
+        "Persia Navy_on_Almost_White Screen Printed",
+        "quotes",
+        "Showroom Line",
+        "Wallcovering"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/scripts/china-seas-onboard/out/tk11076-skips.json b/scripts/china-seas-onboard/out/tk11076-skips.json
new file mode 100644
index 0000000..e2f1a2b
--- /dev/null
+++ b/scripts/china-seas-onboard/out/tk11076-skips.json
@@ -0,0 +1,7 @@
+[
+  {
+    "handle": "dwc",
+    "mfr": "DWC",
+    "reason": "no-anchor"
+  }
+]
\ No newline at end of file
diff --git a/scripts/china-seas-onboard/tk11076-remediate.mjs b/scripts/china-seas-onboard/tk11076-remediate.mjs
new file mode 100644
index 0000000..a20ff68
--- /dev/null
+++ b/scripts/china-seas-onboard/tk11076-remediate.mjs
@@ -0,0 +1,280 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 — batch-correct LIVE China Seas products (vendor="China Seas").
+ *  ANCHOR: authoritative real colorway = china_seas_catalog.color_name (mac2, /tmp socket),
+ *          resolved per-product by shopify_product_id (gid) primary, manufacturer_sku fallback.
+ *
+ *  Operation per matched product:
+ *   (1) SET the correct color:<real colorway> facet (from DB color_name, verbatim).
+ *   (2) STRIP AI-palette STRAY color-word tags that are NOT the product's real colorway:
+ *         - "Background Color X" tags (AI enrichment noise),
+ *         - bare color-lexicon words/phrases (Beige, Coral, Navy, Light Blue, Sage, ...) that
+ *           are NOT contained in the real colorway string.
+ *   (3) PRESERVE: the real color facet + bare colorway word; the pattern name; ALL style/motif/
+ *       material, collection, vendor, display_variant, quotes, Sample, Needs-x, Showroom, Sold Per Unit,
+ *       New Arrival, Trending, etc. non-color tags. And any color-word that IS genuinely in the real
+ *       colorway (e.g. real colorway "Navy on White" -> keep "Navy" and "White").
+ *
+ *  NO variant SKU / price / status / sales-channel / image writes. Tags only.
+ *  PG-first: nothing to write in china_seas_catalog (color_name is already authoritative) — we record
+ *  the old tag array to THIS vendor's own rollback file, then write Shopify (authoritative). Ledgered.
+ *
+ *   node tk11076-remediate.mjs                    # DRY-RUN report only
+ *   node tk11076-remediate.mjs --canary=6         # DRY-RUN, diverse 6-product before/after diff -> canary.json
+ *   node tk11076-remediate.mjs --apply --limit=6  # apply first N of plan (used only after canary judged safe)
+ *   node tk11076-remediate.mjs --apply            # full batch (90s gap every 25)
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execSync } from 'node:child_process';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+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 LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+const CANARY = parseInt((process.argv.find(a => a.startsWith('--canary=')) || '').split('=')[1] || '0', 10);
+const ROLLBACK = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/TK-11076-China_Seas-rollback.json');
+const LEDGER_DIR = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible');
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const decode = s => String(s || '').replace(/&amp;/g, '&').replace(/&#39;/g, "'").replace(/&quot;/g, '"');
+const norm = s => decode(s).toLowerCase().replace(/\s*\/\s*/g, '/').replace(/\s+/g, ' ').trim();
+
+// ---- color lexicon: single-token base color words + modifiers ----
+const COLOR_MODIFIERS = new Set(['soft','pale','dark','light','deep','bright','warm','cool','dusty',
+  'metallic','metalic','antique','burnished','muted','rich','new','ii','iii','matte']);
+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','tint','tan',
+  'silver','gold','gilver','bronze','copper','pewter','platinum','mica','graphite','charcoal','slate','mylar','grasscloth',
+  'teal','aqua','turquoise','jade','celadon','sage','mint','emerald','forest','olive','chartreuse','lime','green','celery',
+  'blue','navy','indigo','cobalt','sapphire','cornflower','sky','denim','powder','peacock','azure','periwinkle','artic','arctic','cadet',
+  'red','crimson','scarlet','burgundy','wine','russet','terracotta','rust','brick','coral','salmon','blush','merlot','sienna',
+  'pink','rose','fuchsia','magenta','lavender','lilac','purple','plum','amethyst','mauve','violet',
+  'yellow','ochre','saffron','mustard','honey','amber','peach','apricot','walnut','espresso','coffee','butter','tangerine','orange',
+  'brown','chocolate','khaki','ecru','multi','natural','neutral','midnight','seafoam','ash','camel','hazelnut','pastel','cactus','bali']);
+// A live tag is a "bare color word/phrase" (palette-noise candidate) if EVERY space/slash token is a
+// color word or modifier (e.g. "Light Blue", "Sea Green", "Navy", "Off-White"), i.e. it carries no
+// pattern/motif/material meaning. "Off White" / "Off-White" handled by treating "off" as a modifier here:
+COLOR_MODIFIERS.add('off');
+function isBareColorTag(tag) {
+  const t = norm(tag);
+  if (!t) return false;
+  const toks = t.split(/[\s/]+/).filter(Boolean);
+  if (!toks.length) return false;
+  const allColor = toks.every(p => COLOR_WORDS.has(p) || COLOR_MODIFIERS.has(p));
+  if (!allColor) return false;
+  return toks.some(p => COLOR_WORDS.has(p)); // must contain >=1 real color word (not modifiers-only)
+}
+// Set of color tokens that appear in THIS product's real colorway — these are legit and preserved.
+function colorwayTokenSet(colorway) {
+  const s = new Set();
+  for (const p of norm(colorway).split(/[\s/]+/).filter(Boolean)) s.add(p);
+  return s;
+}
+// Facet value convention: "color:<Colorway>" title-cased, preserving the DB colorway phrasing.
+const titleCasePhrase = s => decode(s).replace(/\s+/g, ' ').trim()
+  .replace(/\b([a-z])/g, c => c.toUpperCase());
+
+async function gql(query, variables) {
+  for (let a = 0; a < 5; 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) });
+    const j = await res.json().catch(() => ({}));
+    if (j.errors) { if (a === 4) return { errors: j.errors }; await sleep(1200 * (a + 1)); continue; }
+    await sleep(350); return j.data;
+  }
+}
+
+async function fetchLive() {
+  const out = []; let cursor = null;
+  const q = `query($cursor:String){
+    products(first:100, query:"vendor:'China Seas'", after:$cursor){
+      pageInfo{ hasNextPage endCursor }
+      nodes{ id handle status title tags
+        mfr: metafield(namespace:"custom", key:"manufacturer_sku"){ value }
+        variants(first:1){ nodes{ sku } } }
+    } }`;
+  for (let guard = 0; guard < 40; guard++) {
+    const d = await gql(q, { cursor });
+    if (!d || d.errors) { console.error('fetchLive gql error', JSON.stringify(d && d.errors)); break; }
+    for (const n of d.products.nodes) out.push(n);
+    if (!d.products.pageInfo.hasNextPage) break;
+    cursor = d.products.pageInfo.endCursor;
+  }
+  return out;
+}
+
+// DB anchor: gid -> {colorway,bg}, mfr_sku(upper) -> {colorway,bg}
+function anchorMaps() {
+  const raw = execSync(`psql "${DB}" -tAc "select coalesce(json_agg(json_build_object('gid',shopify_product_id,'mfr',mfr_sku,'cw',color_name,'bg',ai_background_color))::text,'[]') from china_seas_catalog where color_name is not null and color_name<>''"`,
+    { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 }).trim();
+  const byGid = new Map(), byMfr = new Map();
+  for (const r of JSON.parse(raw || '[]')) {
+    const rec = { colorway: (r.cw || '').replace(/\s+/g, ' ').trim(), bg: (r.bg || '').trim() };
+    if (r.gid) byGid.set(String(r.gid), rec);
+    if (r.mfr) byMfr.set(String(r.mfr).toUpperCase(), rec);
+  }
+  return { byGid, byMfr };
+}
+
+function planFor(p, anchor) {
+  const gid = p.id;
+  const mfr = (p.mfr && p.mfr.value) || (p.variants.nodes.map(v => v.sku).find(Boolean) || '')
+    .replace(/-Sample$/i, '').replace(/^DW[A-Z]{2}-/i, '');
+  const mfrU = String(mfr).toUpperCase();
+  const rec = anchor.byGid.get(gid) || anchor.byMfr.get(mfrU);
+  if (!rec || !rec.colorway) return { skip: 'no-anchor', mfr: mfrU };
+
+  const colorway = rec.colorway;                          // authoritative real colorway
+  const facet = 'color:' + titleCasePhrase(colorway);
+  const bareColorway = titleCasePhrase(colorway);
+  const cwTokens = colorwayTokenSet(colorway);            // legit color tokens to preserve
+
+  const newTags = [];
+  const strippedPalette = [], strippedBg = [];
+  const seen = new Set();
+  for (const t of p.tags) {
+    const key = norm(t);
+    // 1) strip "Background Color X"
+    if (/^background\s+color\b/i.test(t)) { strippedBg.push(t); continue; }
+    // 2) strip any pre-existing color:/colorway: facet (will re-add the correct one)
+    if (/^colou?r(way)?:/i.test(t)) { strippedPalette.push(t); continue; }
+    // 3) strip bare color-word tags that are NOT (part of) the real colorway.
+    if (isBareColorTag(t)) {
+      // keep it only if it IS the full colorway, or every token of it is a token of the real colorway
+      const toks = key.split(/[\s/]+/).filter(Boolean);
+      const isColorwaySelf = key === norm(bareColorway);
+      const allInColorway = toks.every(x => cwTokens.has(x));
+      if (isColorwaySelf || allInColorway) {
+        if (!seen.has(key)) { newTags.push(t); seen.add(key); }
+        continue;
+      }
+      strippedPalette.push(t); continue;
+    }
+    // 4) PRESERVE every non-color tag
+    if (!seen.has(key)) { newTags.push(t); seen.add(key); }
+  }
+  // ADD the correct facet + bare colorway
+  if (!seen.has(norm(facet))) { newTags.push(facet); seen.add(norm(facet)); }
+  if (!seen.has(norm(bareColorway))) { newTags.push(bareColorway); seen.add(norm(bareColorway)); }
+
+  const changed = JSON.stringify([...p.tags].sort()) !== JSON.stringify([...newTags].sort());
+  return { skip: null, gid, handle: p.handle, status: p.status, mfr: mfrU, title: p.title,
+    colorway, facet, oldTags: p.tags, newTags, strippedPalette, strippedBg, changed };
+}
+
+async function run() {
+  const anchor = anchorMaps();
+  let live = await fetchLive();
+  console.log(`live China Seas products: ${live.length}  |  DB anchor rows: byGid=${anchor.byGid.size} byMfr=${anchor.byMfr.size}`);
+
+  const plans = [], skipped = [];
+  for (const p of live) {
+    const pl = planFor(p, anchor);
+    if (pl.skip) { skipped.push({ handle: p.handle, mfr: pl.mfr, reason: pl.skip }); continue; }
+    if (!pl.changed) continue;
+    plans.push(pl);
+  }
+  console.log(`plans (need change): ${plans.length}   skipped (no confident anchor): ${skipped.length}`);
+  fs.mkdirSync(path.join(HERE, 'out'), { recursive: true });
+  fs.writeFileSync(path.join(HERE, 'out', 'tk11076-skips.json'), JSON.stringify(skipped, null, 2));
+
+  // ---- CANARY MODE: diverse before/after diff, JUDGE safety, no writes ----
+  if (CANARY && !APPLY) {
+    // pick diverse: spread across the plan by index + include multi-word colorways + strip-heavy
+    const sorted = [...plans].sort((a, b) => b.strippedPalette.length - a.strippedPalette.length);
+    const picks = [];
+    const idxs = new Set();
+    const spread = Math.max(1, Math.floor(plans.length / CANARY));
+    for (let i = 0; i < plans.length && picks.length < CANARY; i += spread) { picks.push(plans[i]); idxs.add(plans[i].handle); }
+    for (const pl of sorted) { if (picks.length >= CANARY) break; if (!idxs.has(pl.handle)) { picks.push(pl); idxs.add(pl.handle); } }
+    const diff = picks.slice(0, CANARY).map(pl => ({
+      handle: pl.handle, status: pl.status, mfr: pl.mfr, title: pl.title,
+      real_colorway: pl.colorway, facet_added: pl.facet,
+      before_tags: pl.oldTags, after_tags: pl.newTags,
+      stripped_palette: pl.strippedPalette, stripped_bg: pl.strippedBg,
+      preserved_noncolor: pl.newTags.filter(t => !isBareColorTag(t) && !/^colou?r(way)?:/i.test(t)),
+    }));
+    // SAFETY JUDGE: would we strip a color word that is genuinely a color of the product (i.e. present
+    // in the real colorway)? Our logic already preserves colorway tokens, so any stripped token that is
+    // in cwTokens is a violation. Compute it explicitly as the fail signal.
+    let violations = 0;
+    for (const pl of picks) {
+      const cwTokens = colorwayTokenSet(pl.colorway);
+      for (const t of pl.strippedPalette) {
+        const toks = norm(t).split(/[\s/]+/).filter(Boolean);
+        if (toks.some(x => cwTokens.has(x)) && toks.every(x => cwTokens.has(x))) violations++;
+      }
+    }
+    fs.writeFileSync(path.join(HERE, 'out', 'tk11076-canary.json'),
+      JSON.stringify({ picked: diff.length, would_strip_legit_colorway_token: violations, diff }, null, 2));
+    console.log(`CANARY: ${diff.length} products written to out/tk11076-canary.json  |  legit-colorway-token strips: ${violations}`);
+    console.log(violations === 0 ? 'CANARY VERDICT: SAFE (no legit colorway token would be stripped).'
+      : 'CANARY VERDICT: UNSAFE — legit colorway tokens would be stripped. DO NOT run batch.');
+    return { canary: true, violations, plans: plans.length };
+  }
+
+  // sample dry-run print
+  const show = (LIMIT ? plans.slice(0, LIMIT) : plans).slice(0, 8);
+  for (const pl of show) {
+    const add = pl.newTags.filter(t => !pl.oldTags.includes(t));
+    const rem = pl.oldTags.filter(t => !pl.newTags.includes(t));
+    console.log(`\n[${pl.status}] ${pl.handle} (${pl.mfr}) "${pl.title}" -> ${pl.facet}`);
+    console.log(`   +[${add.join(', ')}]  -[${rem.join(', ')}]`);
+  }
+
+  if (!APPLY) { console.log(`\nDRY-RUN. ${plans.length} would change. Re-run with --apply.`); return { plans: plans.length }; }
+
+  // ---- APPLY ----
+  let toApply = LIMIT ? plans.slice(0, LIMIT) : plans;
+  const rollback = fs.existsSync(ROLLBACK) ? JSON.parse(fs.readFileSync(ROLLBACK, 'utf8')) : {};
+  let done = 0, pass = 0, fail = 0, facetsSet = 0, strays = 0, batchCount = 0;
+  for (const pl of toApply) {
+    rollback[pl.handle] = { id: pl.gid, mfr: pl.mfr, ts: new Date().toISOString(),
+      old_tags: pl.oldTags, new_tags: pl.newTags, colorway: pl.colorway };
+    fs.writeFileSync(ROLLBACK, JSON.stringify(rollback, null, 2));
+
+    const d = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id } userErrors{ field message } } }`,
+      { input: { id: pl.gid, tags: pl.newTags } });
+    const ue = d && d.productUpdate && d.productUpdate.userErrors;
+    if (ue && ue.length) { console.error(`   userErrors ${pl.handle}: ${JSON.stringify(ue)}`); fail++; continue; }
+
+    // per-item VERIFY (re-GET): facet present, strays gone, no legit/non-color tag lost
+    const v = await gql(`query($id:ID!){ product(id:$id){ tags status } }`, { id: pl.gid });
+    const liveTags = (v && v.product && v.product.tags) || [];
+    const facetOk = liveTags.includes(pl.facet);
+    const straysGone = !pl.strippedPalette.some(t => liveTags.includes(t))
+      && !pl.strippedBg.some(t => liveTags.includes(t));
+    const nonColorPreserved = pl.newTags.filter(t => !isBareColorTag(t) && !/^colou?r(way)?:/i.test(t)
+      && !/^background\s+color/i.test(t)).every(t => liveTags.includes(t));
+    const verifyOk = facetOk && straysGone && nonColorPreserved;
+    if (verifyOk) { pass++; facetsSet++; strays += pl.strippedPalette.length + pl.strippedBg.length; }
+    else { fail++; console.error(`   VERIFY FAIL ${pl.handle}: facetOk=${facetOk} straysGone=${straysGone} nonColorPreserved=${nonColorPreserved}`); }
+
+    done++;
+    console.log(`[${done}/${toApply.length}] ${pl.handle} ${verifyOk ? '✓ verified' : 'VERIFY-FAIL'}`);
+    try {
+      execSync(`node "${path.join(LEDGER_DIR, 'log-exec.mjs')}" ` +
+        `--agent vp-dw-commerce --ticket TK-11076 ` +
+        `--action "China Seas retag ${pl.handle}: ${pl.facet}, strip AI-palette strays" ` +
+        `--blast 1 ` +
+        `--undo "cd ${HERE} && node tk11076-rollback.mjs --only=${pl.handle} --apply" ` +
+        `--verify "GET product ${pl.gid} — color facet correct, palette strays gone, non-color tags intact"`,
+        { stdio: 'ignore' });
+    } catch { /* ledger best-effort */ }
+
+    batchCount++;
+    if (batchCount % 25 === 0 && done < toApply.length) { console.log('   …90s inter-batch gap…'); await sleep(90000); }
+    else await sleep(600);
+  }
+  console.log(`\nDONE: ${done} products · verify-pass ${pass} · verify-fail ${fail} · facetsSet ${facetsSet} · straysStripped ${strays}`);
+  console.log(`rollback: ${ROLLBACK}`);
+  return { done, pass, fail, facetsSet, strays };
+}
+run();
diff --git a/scripts/china-seas-onboard/tk11076-rollback.mjs b/scripts/china-seas-onboard/tk11076-rollback.mjs
new file mode 100644
index 0000000..eae73aa
--- /dev/null
+++ b/scripts/china-seas-onboard/tk11076-rollback.mjs
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 China Seas rollback — restore products to pre-remediation tags from
+ * ~/.claude/yolo-queue/executed-reversible/TK-11076-China_Seas-rollback.json (THIS vendor's own file).
+ * Tags-only (retag-only remediation; no image/PG writes to reverse).
+ *
+ *   node tk11076-rollback.mjs                    # DRY-RUN, all recorded handles
+ *   node tk11076-rollback.mjs --only=h1,h2       # DRY-RUN specific handles
+ *   node tk11076-rollback.mjs --apply            # RESTORE all
+ *   node tk11076-rollback.mjs --only=h1 --apply  # RESTORE one
+ */
+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 ONLY = ((process.argv.find(a => a.startsWith('--only=')) || '').split('=')[1] || '').split(',').filter(Boolean);
+const ROLLBACK = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/TK-11076-China_Seas-rollback.json');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+async function gql(query, variables) {
+  for (let a = 0; a < 5; 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) });
+    const j = await res.json().catch(() => ({}));
+    if (j.errors) { if (a === 4) return { errors: j.errors }; await sleep(1200 * (a + 1)); continue; }
+    await sleep(350); return j.data;
+  }
+}
+
+async function main() {
+  const map = JSON.parse(fs.readFileSync(ROLLBACK, 'utf8'));
+  const handles = ONLY.length ? ONLY : Object.keys(map);
+  for (const h of handles) {
+    const r = map[h];
+    if (!r) { console.log(`no rollback record for ${h}`); continue; }
+    console.log(`[${h}] restore tags(${r.old_tags.length})`);
+    if (!APPLY) continue;
+    const d = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ userErrors{ message } } }`,
+      { input: { id: r.id, tags: r.old_tags } });
+    const ue = d && d.productUpdate && d.productUpdate.userErrors;
+    if (ue && ue.length) console.error(`   errors: ${JSON.stringify(ue)}`); else console.log('   restored ✓');
+  }
+  if (!APPLY) console.log('\nDRY-RUN. Re-run with --apply to restore.');
+}
+main();

← 672d319 TK-11076: PJ retag halted at canary gate (stopped-canary-uns  ·  back to Designerwallcoverings  ·  TK-11076: Kravet colorway retag scripts + canary halt (ancho 8e552b1 →