← 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
A scripts/china-seas-onboard/out/tk11076-canary.jsonA scripts/china-seas-onboard/out/tk11076-skips.jsonA scripts/china-seas-onboard/tk11076-remediate.mjsA scripts/china-seas-onboard/tk11076-rollback.mjs
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(/&/g, '&').replace(/'/g, "'").replace(/"/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 →