← back to Designerwallcoverings
TK-11073 Wave1: Osborne LIVE remediation — 29 real-title products retagged
b61983c5de63458a44e38c551eb45447daa456ca · 2026-09-01 11:56:04 -0700 · Steve Abrams
Live remediation of Osborne & Little products carrying AI-guessed color: facets + stray
AI-palette bare tags. Colorway derived from each product's OWN live title (title − DB pattern_name),
gated on a color-lexicon confidence check; wrong facets + own-metafield-sourced palette strays
stripped; all non-color tags preserved; NO image writes. 7 confident facet-adds, 22 strip-only.
Generic '#SKU'-title products (94) deliberately UNTOUCHED (no real colorway; their palette tags
are their only color signal — separate backfill follow-up) per codex ruling. Reversible via
tk11073-rollback.mjs, ledgered, per-item re-GET verified (29/29 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/osborne-onboard/out/tk11073-skips.jsonA scripts/osborne-onboard/tk11073-remediate.mjsA scripts/osborne-onboard/tk11073-rollback.mjs
Diff
commit b61983c5de63458a44e38c551eb45447daa456ca
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 11:56:04 2026 -0700
TK-11073 Wave1: Osborne LIVE remediation — 29 real-title products retagged
Live remediation of Osborne & Little products carrying AI-guessed color: facets + stray
AI-palette bare tags. Colorway derived from each product's OWN live title (title − DB pattern_name),
gated on a color-lexicon confidence check; wrong facets + own-metafield-sourced palette strays
stripped; all non-color tags preserved; NO image writes. 7 confident facet-adds, 22 strip-only.
Generic '#SKU'-title products (94) deliberately UNTOUCHED (no real colorway; their palette tags
are their only color signal — separate backfill follow-up) per codex ruling. Reversible via
tk11073-rollback.mjs, ledgered, per-item re-GET verified (29/29 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/osborne-onboard/out/tk11073-skips.json | 475 +++++++++++++++++++++++++
scripts/osborne-onboard/tk11073-remediate.mjs | 269 ++++++++++++++
scripts/osborne-onboard/tk11073-rollback.mjs | 50 +++
3 files changed, 794 insertions(+)
diff --git a/scripts/osborne-onboard/out/tk11073-skips.json b/scripts/osborne-onboard/out/tk11073-skips.json
new file mode 100644
index 0000000..c6e5004
--- /dev/null
+++ b/scripts/osborne-onboard/out/tk11073-skips.json
@@ -0,0 +1,475 @@
+{
+ "skippedAdd": [],
+ "skippedGeneric": [
+ {
+ "handle": "w7190-06_1-jpg",
+ "mfr": "W7190-06",
+ "title": "Osborne & Little Wallcovering #W7190-06"
+ },
+ {
+ "handle": "w7190-07_1-jpg",
+ "mfr": "W7190-07",
+ "title": "Osborne & Little Wallcovering #W7190-07"
+ },
+ {
+ "handle": "w7190-08_1-jpg",
+ "mfr": "W7190-08",
+ "title": "Osborne & Little Wallcovering #W7190-08"
+ },
+ {
+ "handle": "w7190-09_1-jpg",
+ "mfr": "W7190-09",
+ "title": "Osborne & Little Wallcovering #W7190-09"
+ },
+ {
+ "handle": "w7190-10_1-jpg",
+ "mfr": "W7190-10",
+ "title": "Osborne & Little Wallcovering #W7190-10"
+ },
+ {
+ "handle": "w7190-11_1-jpg",
+ "mfr": "W7190-11",
+ "title": "Osborne & Little Wallcovering #W7190-11"
+ },
+ {
+ "handle": "w7191-01_1-jpg",
+ "mfr": "W7191-01",
+ "title": "Osborne & Little Wallcovering #W7191-01"
+ },
+ {
+ "handle": "w7191-03_1-jpg",
+ "mfr": "W7191-03",
+ "title": "Osborne & Little Wallcovering #W7191-03"
+ },
+ {
+ "handle": "w7192-01_1-jpg",
+ "mfr": "W7192-01",
+ "title": "Osborne & Little Wallcovering #W7192-01"
+ },
+ {
+ "handle": "w7193-01_1-jpg",
+ "mfr": "W7193-01",
+ "title": "Osborne & Little Wallcovering #W7193-01"
+ },
+ {
+ "handle": "w7193-05_1-jpg",
+ "mfr": "W7193-05",
+ "title": "Osborne & Little Wallcovering #W7193-05"
+ },
+ {
+ "handle": "w7194-02_1-jpg",
+ "mfr": "W7194-02",
+ "title": "Osborne & Little Wallcovering #W7194-02"
+ },
+ {
+ "handle": "w7215-01_1-jpg",
+ "mfr": "W7215-01",
+ "title": "Osborne & Little Wallcovering #W7215-01"
+ },
+ {
+ "handle": "w7218-01_1-jpg",
+ "mfr": "W7218-01",
+ "title": "Osborne & Little Wallcovering #W7218-01"
+ },
+ {
+ "handle": "w7219-01_1-jpg",
+ "mfr": "W7219-01",
+ "title": "Osborne & Little Wallcovering #W7219-01"
+ },
+ {
+ "handle": "w7260-01-jpg",
+ "mfr": "W7260-01",
+ "title": "Osborne & Little Wallcovering #W7260-01"
+ },
+ {
+ "handle": "w7267-01-jpg",
+ "mfr": "W7267-01",
+ "title": "Osborne & Little Wallcovering #W7267-01"
+ },
+ {
+ "handle": "w7268-01-jpg",
+ "mfr": "W7268-01",
+ "title": "Osborne & Little Wallcovering #W7268-01"
+ },
+ {
+ "handle": "w7331-03-jpg",
+ "mfr": "W7331-03",
+ "title": "Osborne & Little Wallcovering #W7331-03"
+ },
+ {
+ "handle": "w7333-03-jpg",
+ "mfr": "W7333-03",
+ "title": "Osborne & Little Wallcovering #W7333-03"
+ },
+ {
+ "handle": "w7335-01-jpg",
+ "mfr": "W7335-01",
+ "title": "Osborne & Little Wallcovering #W7335-01"
+ },
+ {
+ "handle": "w7335-02-jpg",
+ "mfr": "W7335-02",
+ "title": "Osborne & Little Wallcovering #W7335-02"
+ },
+ {
+ "handle": "w7336-01-jpg",
+ "mfr": "W7336-01",
+ "title": "Osborne & Little Wallcovering #W7336-01"
+ },
+ {
+ "handle": "w7337-02-jpg",
+ "mfr": "W7337-02",
+ "title": "Osborne & Little Wallcovering #W7337-02"
+ },
+ {
+ "handle": "w7338-02-jpg",
+ "mfr": "W7338-02",
+ "title": "Osborne & Little Wallcovering #W7338-02"
+ },
+ {
+ "handle": "w7339-02-jpg",
+ "mfr": "W7339-02",
+ "title": "Osborne & Little Wallcovering #W7339-02"
+ },
+ {
+ "handle": "w7339-03_2-jpg",
+ "mfr": "W7339-03",
+ "title": "Osborne & Little Wallcovering #W7339-03"
+ },
+ {
+ "handle": "w7340-01-jpg",
+ "mfr": "W7340-01",
+ "title": "Osborne & Little Wallcovering #W7340-01"
+ },
+ {
+ "handle": "w7350-01_3-jpg",
+ "mfr": "W7350-01",
+ "title": "Osborne & Little Wallcovering #W7350-01"
+ },
+ {
+ "handle": "w7360-04-jpg",
+ "mfr": "W7360-04",
+ "title": "Osborne & Little Wallcovering #W7360-04"
+ },
+ {
+ "handle": "w7360-06-jpg",
+ "mfr": "W7360-06",
+ "title": "Osborne & Little Wallcovering #W7360-06"
+ },
+ {
+ "handle": "w7360-11-jpg",
+ "mfr": "W7360-11",
+ "title": "Osborne & Little Wallcovering #W7360-11"
+ },
+ {
+ "handle": "w7360-12-jpg",
+ "mfr": "W7360-12",
+ "title": "Osborne & Little Wallcovering #W7360-12"
+ },
+ {
+ "handle": "w7360-15-jpg",
+ "mfr": "W7360-15",
+ "title": "Osborne & Little Wallcovering #W7360-15"
+ },
+ {
+ "handle": "w7360-17-jpg",
+ "mfr": "W7360-17",
+ "title": "Osborne & Little Wallcovering #W7360-17"
+ },
+ {
+ "handle": "w7360-18-jpg",
+ "mfr": "W7360-18",
+ "title": "Osborne & Little Wallcovering #W7360-18"
+ },
+ {
+ "handle": "w7360-19-jpg",
+ "mfr": "W7360-19",
+ "title": "Osborne & Little Wallcovering #W7360-19"
+ },
+ {
+ "handle": "w7360-21-jpg",
+ "mfr": "W7360-21",
+ "title": "Osborne & Little Wallcovering #W7360-21"
+ },
+ {
+ "handle": "w7360-26-jpg",
+ "mfr": "W7360-26",
+ "title": "Osborne & Little Wallcovering #W7360-26"
+ },
+ {
+ "handle": "w7360-27-jpg",
+ "mfr": "W7360-27",
+ "title": "Osborne & Little Wallcovering #W7360-27"
+ },
+ {
+ "handle": "w7360-28-jpg",
+ "mfr": "W7360-28",
+ "title": "Osborne & Little Wallcovering #W7360-28"
+ },
+ {
+ "handle": "w7360-30-jpg",
+ "mfr": "W7360-30",
+ "title": "Osborne & Little Wallcovering #W7360-30"
+ },
+ {
+ "handle": "w7450-01_8_1-jpg",
+ "mfr": "W7450-01_8",
+ "title": "Osborne & Little Wallcovering #W7450-01"
+ },
+ {
+ "handle": "w7452-01_9_1-jpg",
+ "mfr": "W7452-01_9",
+ "title": "Osborne & Little Wallcovering #W7452-01_9"
+ },
+ {
+ "handle": "w7452-02_9_1-jpg",
+ "mfr": "W7452-02_9",
+ "title": "Osborne & Little Wallcovering #W7452-02_9"
+ },
+ {
+ "handle": "w7455-04_8_1-jpg",
+ "mfr": "W7455-04_8",
+ "title": "Osborne & Little Wallcovering #W7455-04"
+ },
+ {
+ "handle": "w7457-02_5_1-jpg",
+ "mfr": "W7457-02_5",
+ "title": "Osborne & Little Wallcovering #W7457-02"
+ },
+ {
+ "handle": "w7458-01_8_1-jpg",
+ "mfr": "W7458-01_8",
+ "title": "Osborne & Little Wallcovering #W7458-01"
+ },
+ {
+ "handle": "w7458-02_8_1-jpg",
+ "mfr": "W7458-02_8",
+ "title": "Osborne & Little Wallcovering #W7458-02"
+ },
+ {
+ "handle": "w7458-04_8_1-jpg",
+ "mfr": "W7458-04_8",
+ "title": "Osborne & Little Wallcovering #W7458-04"
+ },
+ {
+ "handle": "w7458-05_8_1-jpg",
+ "mfr": "W7458-05_8",
+ "title": "Osborne & Little Wallcovering #W7458-05"
+ },
+ {
+ "handle": "w7460-03_8_1-jpg",
+ "mfr": "W7460-03_8",
+ "title": "Osborne & Little Wallcovering #W7460-03"
+ },
+ {
+ "handle": "w7490-04-jpg",
+ "mfr": "W7490-04",
+ "title": "Osborne & Little Wallcovering #W7490-04"
+ },
+ {
+ "handle": "w7491-01-jpg",
+ "mfr": "W7491-01",
+ "title": "Osborne & Little Wallcovering #W7491-01"
+ },
+ {
+ "handle": "w7491-02-jpg",
+ "mfr": "W7491-02",
+ "title": "Osborne & Little Wallcovering #W7491-02"
+ },
+ {
+ "handle": "w7494-01-jpg",
+ "mfr": "W7494-01",
+ "title": "Osborne & Little Wallcovering #W7494-01"
+ },
+ {
+ "handle": "w7494-03-jpg",
+ "mfr": "W7494-03",
+ "title": "Osborne & Little Wallcovering #W7494-03"
+ },
+ {
+ "handle": "eur-70327-designer-wallcoverings-los-angeles",
+ "mfr": "W6296-07-CARNABY WIDE STRIPE-",
+ "title": "Carnaby Wide Stripe | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70336-designer-wallcoverings-los-angeles",
+ "mfr": "W6301-01-BOA SNAKE -CREAM",
+ "title": "Boa Snake Cream | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70337-designer-wallcoverings-los-angeles",
+ "mfr": "W6301-03-BOA SNAKE -LIGHT GOLD",
+ "title": "Boa Snake Light Gold | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70338-designer-wallcoverings-los-angeles",
+ "mfr": "W6301-04-BOA SNAKE -BRONZE",
+ "title": "Boa Snake Bronze | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70339-designer-wallcoverings-los-angeles",
+ "mfr": "W6301-05-BOA SNAKE -GREY",
+ "title": "Boa Snake Grey | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70344-designer-wallcoverings-los-angeles",
+ "mfr": "W6303-02-MAKO METALLIC PLAIN-",
+ "title": "Mako Metallic Plain | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70345-designer-wallcoverings-los-angeles",
+ "mfr": "W6303-04-MAKO METALLIC PLAIN-",
+ "title": "Mako Metallic Plain | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70346-designer-wallcoverings-los-angeles",
+ "mfr": "W6303-05-MAKO METALLIC PLAIN-",
+ "title": "Mako Metallic Plain | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70352-designer-wallcoverings-los-angeles",
+ "mfr": "EUR-70352-DESIGNER-WALLCOVERINGS-LOS-ANGELES",
+ "title": "Chameleon Glittering holographic diamonds Grey | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70354-designer-wallcoverings-los-angeles",
+ "mfr": "W6306--PANTHERA PANTHER STRIPE-",
+ "title": "Panthera panther stripe | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70392-designer-wallcoverings-los-angeles",
+ "mfr": "W6337-01-CROCODILO CROCODILE-",
+ "title": "Crocodilo Crocodile | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70393-designer-wallcoverings-los-angeles",
+ "mfr": "W6337-02-CROCODILO CROCODILE-",
+ "title": "Crocodilo Crocodile | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70395-designer-wallcoverings-los-angeles",
+ "mfr": "W6337-04-CROCODILO CROCODILE-",
+ "title": "Crocodilo Crocodile | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70401-designer-wallcoverings-los-angeles",
+ "mfr": "EUR-70401-DESIGNER-WALLCOVERINGS-LOS-ANGELES",
+ "title": "Rabanna Holographic Holographic foil on a non-woven base resembles grass cloth. | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70403-designer-wallcoverings-los-angeles",
+ "mfr": "EUR-70403-DESIGNER-WALLCOVERINGS-LOS-ANGELES",
+ "title": "Canestra Basketweave Textured basket weave on non-woven. | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70404-designer-wallcoverings-los-angeles",
+ "mfr": "EUR-70404-DESIGNER-WALLCOVERINGS-LOS-ANGELES",
+ "title": "Canestra Basketweave Textured basket weave on non-woven. | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70407-designer-wallcoverings-los-angeles",
+ "mfr": "W6342-04-HOUNDSTOOTH SMALL SCALE-",
+ "title": "Houndstooth Small Scale | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70411-designer-wallcoverings-los-angeles",
+ "mfr": "W6344-01-ZANELLA DOTS HOLOGRAPH-",
+ "title": "Zanella Dots Holograph | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70412-designer-wallcoverings-los-angeles",
+ "mfr": "W6344-03-ZANELLA DOTS HOLOGRAPH-",
+ "title": "Zanella Dots Holograph | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70413-designer-wallcoverings-los-angeles",
+ "mfr": "W6345-04-CORDOVAN GOAT SKIN-",
+ "title": "Cordovan Goat Skin | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70414-designer-wallcoverings-los-angeles",
+ "mfr": "W6345-05-CORDOVAN GOAT SKIN-",
+ "title": "Cordovan Goat Skin | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70419-designer-wallcoverings-los-angeles",
+ "mfr": "W6430-01-FEUILLE DE CHENE OAK LEAVES-",
+ "title": "Feuille de Chene Oak Leaves | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70420-designer-wallcoverings-los-angeles",
+ "mfr": "W6430-04-FEUILLE DE CHENE OAK LEAVES-",
+ "title": "Feuille de Chene Oak Leaves | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70421-designer-wallcoverings-los-angeles",
+ "mfr": "W6430-05-FEUILLE DE CHENE OAK LEAVES-",
+ "title": "Feuille de Chene Oak Leaves | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70447-designer-wallcoverings-los-angeles",
+ "mfr": "W6438--PAMPILLE STAR HOLOGRAPHIC -",
+ "title": "Pampille Star Holographic | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70523-designer-wallcoverings-los-angeles",
+ "mfr": "W6582-06-ZINGRINA SHAGREEN-",
+ "title": "Zingrina shagreen | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70568-designer-wallcoverings-los-angeles",
+ "mfr": "EUR-70568-DESIGNER-WALLCOVERINGS-LOS-ANGELES",
+ "title": "Latania Small Scale Paisley Chartreuse Yellow | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70759-designer-wallcoverings-los-angeles",
+ "mfr": "W7141-1-ORANGERY BIRDCAGE NOVELTY-FUCHSIA",
+ "title": "Orangery Birdcage Novelty Fuchsia | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70761-designer-wallcoverings-los-angeles",
+ "mfr": "W7141-3-ORANGERY BIRDCAGE NOVELTY-YELLOW",
+ "title": "Orangery Birdcage Novelty Yellow | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-70846-designer-wallcoverings-los-angeles",
+ "mfr": "W7261-04",
+ "title": "Adornado - Blue Mauve Wallcovering | Osborne & Little"
+ },
+ {
+ "handle": "eur-71337-designer-wallcoverings-los-angeles",
+ "mfr": "CW5410-08-QUARTZ-",
+ "title": "Quartz | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71339-designer-wallcoverings-los-angeles",
+ "mfr": "CW5410-11-QUARTZ-",
+ "title": "Quartz | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71343-designer-wallcoverings-los-angeles",
+ "mfr": "CW5410-17-QUARTZ-",
+ "title": "Quartz | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71346-designer-wallcoverings-los-angeles",
+ "mfr": "CW6002-04-ZEBRANO-CREAM",
+ "title": "Zebrano Cream | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71350-designer-wallcoverings-los-angeles",
+ "mfr": "CW6005-04-PETRA-CREAM",
+ "title": "Petra Cream | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71351-designer-wallcoverings-los-angeles",
+ "mfr": "CW6006-03-ORATORIO PLASTER-CREAM",
+ "title": "Oratorio Plaster Cream | Osborne & Little Europe"
+ },
+ {
+ "handle": "eur-71364-designer-wallcoverings-los-angeles",
+ "mfr": "VW5811-03-EDO VINYL-CREAM",
+ "title": "Edo Vinyl Cream | Osborne & Little Europe"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/osborne-onboard/tk11073-remediate.mjs b/scripts/osborne-onboard/tk11073-remediate.mjs
new file mode 100644
index 0000000..a34ae38
--- /dev/null
+++ b/scripts/osborne-onboard/tk11073-remediate.mjs
@@ -0,0 +1,269 @@
+#!/usr/bin/env node
+/**
+ * TK-11073 Wave 1 — batch-correct LIVE Osborne & Little products.
+ * • Set the correct color:<real colorway> facet (colorway = title_core − pattern_name; DB-authoritative).
+ * • Strip WRONG color: facets and bare AI-palette color-word tags (source: this product's own
+ * enriched.jsonl colors[].name + backgroundColor — deterministic, per-product, NOT a global denylist).
+ * • PRESERVE every non-color tag (New Arrival, display_variant, collection, style, material, Showroom…).
+ * • NO image writes (Osborne live images verified own-only per the TK-11073 audit). Tags only.
+ *
+ * SCOPE = only products matched to build-ready.jsonl (real title_core) AND carrying pollution.
+ * Products with a generic "#SKU" title / no resolvable colorway are SKIPPED (never guessed).
+ *
+ * PG-first: nothing to write in osborne_catalog for the tag fix (color_name there is a placeholder
+ * "Colorway NN", not the real colorway — the authoritative colorway lives in the title). We record the
+ * old tag array in the rollback map, then write Shopify (authoritative). Reversible + ledgered.
+ *
+ * node tk11073-remediate.mjs # DRY-RUN report only
+ * node tk11073-remediate.mjs --apply --limit=6 # canary (first N of the plan)
+ * node tk11073-remediate.mjs --apply --only=h1,h2
+ * node tk11073-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 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-11073-rollback-map.json');
+const LEDGER_LOG = 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 titleCase = s => decode(s).replace(/\b\w/g, c => c.toUpperCase()).trim();
+const norm = s => titleCase(s).toLowerCase().replace(/\s*\/\s*/g, '/').replace(/\s+/g, ' ').trim();
+
+// CONFIDENCE-GATED colorway extraction (TK-11073, re-scoped after Wave-1 canary mis-fire).
+// The Osborne live titles are NOT reliably "Pattern Colorway" — the older "…Europe/Showroom Line"
+// import embeds sub-pattern/motif words (Glitter Lattice, Zebra, Birds) between pattern and colorway,
+// and mfr_sku does NOT map to the same colorway across the two overlapping imports. So we derive from
+// the LIVE product's OWN title and only RETURN a colorway when it is HIGH-CONFIDENCE: the trailing
+// token group after the pattern must be entirely color-lexicon words / modifiers / slash-joined colors,
+// with ≥1 base color word and NO motif/sub-pattern word. Otherwise null → NO facet added (skip).
+const COLOR_MODIFIERS = new Set(['soft','pale','dark','light','deep','bright','warm','cool','dusty',
+ 'metallic','metalic','antique','burnished','muted','rich']);
+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',
+ 'silver','gold','gilver','bronze','copper','pewter','platinum','mica','graphite','charcoal','slate',
+ 'teal','aqua','turquoise','jade','celadon','sage','mint','emerald','forest','olive','chartreuse','lime','green',
+ 'blue','navy','indigo','cobalt','sapphire','cornflower','sky','denim','powder','peacock','azure',
+ 'red','crimson','scarlet','burgundy','wine','russet','terracotta','rust','brick','coral','salmon','blush',
+ 'pink','rose','fuchsia','magenta','lavender','lilac','purple','plum','amethyst','mauve','violet',
+ 'yellow','ochre','saffron','mustard','honey','amber','peach','apricot','tan','walnut','espresso','coffee',
+ 'brown','chocolate','khaki','ecru','multi','natural','neutral','midnight','seafoam','ash']);
+function isColorToken(tok) {
+ const t = norm(tok);
+ if (!t) return false;
+ return t.split('/').every(p => COLOR_WORDS.has(p) || COLOR_MODIFIERS.has(p));
+}
+function confidentColorway(liveTitle, patternName) {
+ let t = decode(liveTitle || '');
+ t = t.split('|')[0].trim(); // drop "| Osborne & Little[ Europe]"
+ if (/wallcovering\s*#/i.test(liveTitle)) return null; // generic SKU title → skip
+ t = t.replace(/\bwall\s*covering\b\s*$/i, '').trim(); // drop trailing "Wallcovering"
+ t = t.replace(/^\(limited stock\)\s*/i, '').replace(/\s*-\s*/g, ' ').replace(/^s\s+/i, '').trim();
+ const pat = decode(patternName || '').trim();
+ if (!t || !pat) return null;
+ const tToks = t.split(/\s+/), pToks = pat.split(/\s+/);
+ let tail = null;
+ for (let i = 0; i < tToks.length; i++) {
+ if (norm(tToks[i]) === norm(pToks[0])) {
+ let j = 0;
+ while (j < pToks.length && i + j < tToks.length && norm(tToks[i + j]) === norm(pToks[j])) j++;
+ tail = tToks.slice(i + j); break;
+ }
+ }
+ if (tail == null && norm(tToks[0]) === norm(pToks[0])) tail = tToks.slice(1);
+ if (!tail || !tail.length) return null;
+ // CONFIDENCE GATE: every trailing token must be a color token, and ≥1 must be a base color word.
+ if (!tail.every(isColorToken)) return null; // motif/sub-pattern present → skip
+ const hasBase = tail.some(tok => norm(tok).split('/').some(p => COLOR_WORDS.has(p)));
+ if (!hasBase) return null; // modifiers only → skip
+ if (tail.length > 4) return null; // over-capture guard
+ let cw = tail.join(' ').replace(/\s*\/\s*/g, '/').replace(/\s+/g, ' ').trim();
+ if (/^colorway\b/i.test(cw)) return null; // "Colorway 03" placeholder → skip
+ if (!cw || cw.length > 40) return null;
+ return cw;
+}
+
+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:'Osborne & Little'", after:$cursor){
+ pageInfo{ hasNextPage endCursor }
+ nodes{ id handle status title tags
+ mfr: metafield(namespace:"custom", key:"manufacturer_sku"){ 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 }
+ bg: metafield(namespace:"custom", key:"background_color"){ value }
+ variants(first:3){ nodes{ sku } } }
+ } }`;
+ for (let guard = 0; guard < 60; 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;
+}
+
+function load(file) {
+ const p = path.join(HERE, 'out', file);
+ if (!fs.existsSync(p)) return [];
+ return fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
+}
+
+// mfr_sku(upper) → real pattern_name from osborne_catalog (Mac2-canonical staging)
+function patternMap() {
+ const raw = execSync(`psql "${DB}" -tAc "select coalesce(json_agg(json_build_object('sku',mfr_sku,'pat',pattern_name))::text,'[]') from osborne_catalog"`,
+ { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
+ const m = new Map();
+ for (const r of JSON.parse(raw || '[]')) if (r.sku) m.set(String(r.sku).toUpperCase(), r.pat || '');
+ return m;
+}
+
+function main() {
+ return run();
+}
+async function run() {
+ const patByMfr = patternMap();
+
+ let live = await fetchLive();
+ console.log(`live Osborne products: ${live.length}`);
+ if (ONLY.length) live = live.filter(p => ONLY.includes(p.handle));
+
+ const plans = [], skippedAdd = [], skippedGeneric = [];
+ for (const p of live) {
+ const mfr = (p.mfr && p.mfr.value) || (p.variants.nodes.map(v => v.sku).find(Boolean) || '').replace(/-OSB(-Sample)?$/i, '');
+ const mfrU = String(mfr).toUpperCase();
+ const pattern = patByMfr.get(mfrU) || '';
+
+ // SCOPE GATE (TK-11073, codex-ruled option B): only remediate products with a REAL descriptive title
+ // (pattern + colorway). Generic "Osborne & Little Wallcovering #SKU" products have NO real colorway
+ // anywhere — their AI palette tags are their ONLY color signal, and they are NOT the documented
+ // stray-pollution bug. Stripping them would destroy filterability with no replacement. Leave them
+ // UNTOUCHED and log for a separate metadata-backfill follow-up.
+ const genericTitle = /wallcovering\s*#/i.test(p.title) || !pattern;
+ if (genericTitle) {
+ if (p.tags.some(t => /^color:/i.test(t))) skippedGeneric.push({ handle: p.handle, mfr: mfrU, title: p.title });
+ continue;
+ }
+
+ // PALETTE STRIP SOURCE = THIS product's OWN AI color metafields (custom.color_details / color_N_name /
+ // background_color) — the AI palette the enrich layer actually wrote for THIS product. Source-field
+ // driven, per-product, NOT a global color denylist. This is what makes a strip a "noise" strip.
+ 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(titleCase(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]) if (v) palette.add(titleCase(v));
+
+ // CONFIDENT colorway from the LIVE product's OWN title (gated). null → add NO facet.
+ const colorway = confidentColorway(p.title, pattern);
+ const bareColorway = colorway ? titleCase(colorway) : null;
+
+ // A+B: ALWAYS strip existing color: facets (AI-guessed) + bare AI-palette color-words (own-metafield sourced).
+ const newTags = [];
+ const strippedFacet = [], strippedPalette = [];
+ for (const t of p.tags) {
+ if (/^color:/i.test(t)) { strippedFacet.push(t); continue; }
+ if (palette.has(titleCase(t))) { strippedPalette.push(t); continue; }
+ if (bareColorway && norm(t) === norm(bareColorway)) continue; // dedup, re-added canonical below
+ newTags.push(t); // PRESERVE every non-color tag
+ }
+ // C: ADD the correct color facet ONLY when high-confidence.
+ if (colorway) {
+ const facet = 'color:' + titleCase(colorway);
+ if (!newTags.includes(facet)) newTags.push(facet);
+ if (!newTags.includes(bareColorway)) newTags.push(bareColorway);
+ } else if (strippedFacet.length) {
+ skippedAdd.push({ handle: p.handle, mfr: mfrU, title: p.title, wrong_facet: strippedFacet, reason: 'colorway not confidently isolable — facet stripped, none re-added' });
+ }
+ const changed = JSON.stringify([...p.tags].sort()) !== JSON.stringify([...newTags].sort());
+ if (!changed) continue;
+ plans.push({ id: p.id, handle: p.handle, status: p.status, mfr: mfrU, title: p.title, colorway,
+ oldTags: p.tags, newTags, strippedFacet, strippedPalette });
+ }
+
+ const addCt = plans.filter(pl => pl.colorway).length;
+ console.log(`plans: ${plans.length} real-title products need correction (${addCt} get a confident color: facet added; ${plans.length - addCt} strip-only)`);
+ console.log(`skipped-ADD (real title, colorway not confident — facet stripped, none re-added): ${skippedAdd.length}`);
+ console.log(`skipped-GENERIC (generic #SKU title, untouched — separate backfill follow-up): ${skippedGeneric.length}`);
+ fs.writeFileSync(path.join(HERE, 'out', 'tk11073-skips.json'), JSON.stringify({ skippedAdd, skippedGeneric }, null, 2));
+ if (skippedAdd.length) for (const s of skippedAdd) console.log(` SKIP-ADD ${s.handle} "${s.title}" wrong_facet=${JSON.stringify(s.wrong_facet)}`);
+
+ let toApply = plans;
+ if (LIMIT) toApply = plans.slice(0, LIMIT);
+ for (const pl of (LIMIT ? toApply : plans)) {
+ 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}" → color:${pl.colorway || '(none)'}`);
+ console.log(` +[${add.join(', ')}] -[${rem.join(', ')}]`);
+ }
+
+ if (!APPLY) { console.log(`\nDRY-RUN. ${plans.length} would change (applying ${toApply.length}). Re-run with --apply.`); return; }
+
+ // ---- APPLY ----
+ const rollback = fs.existsSync(ROLLBACK) ? JSON.parse(fs.readFileSync(ROLLBACK, 'utf8')) : {};
+ let done = 0, pass = 0, fail = 0, batchCount = 0;
+ for (const pl of toApply) {
+ rollback[pl.handle] = { id: pl.id, mfr: pl.mfr, ts: new Date().toISOString(), old_tags: pl.oldTags, new_tags: pl.newTags };
+ 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.id, 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 tags
+ const v = await gql(`query($id:ID!){ product(id:$id){ tags } }`, { id: pl.id });
+ const liveTags = (v && v.product && v.product.tags) || [];
+ const facetOk = !pl.colorway || liveTags.includes('color:' + titleCase(pl.colorway));
+ const noWrongFacet = !liveTags.some(t => /^color:/i.test(t) && (!pl.colorway || norm(t) !== norm('color:' + pl.colorway)));
+ // A stripped palette word is legitimately KEPT only if it IS the confident bare colorway we re-added
+ // (e.g. colorway "Silver" — the bare "Silver" tag stays). Exclude that one from the gone-check.
+ const keepColorway = pl.colorway ? titleCase(pl.colorway) : null;
+ const paletteGone = !pl.strippedPalette.some(t => liveTags.includes(t) && titleCase(t) !== keepColorway);
+ const verifyOk = facetOk && noWrongFacet && paletteGone;
+ if (verifyOk) pass++; else { fail++; console.error(` VERIFY FAIL ${pl.handle}: facetOk=${facetOk} noWrongFacet=${noWrongFacet} paletteGone=${paletteGone}`); }
+
+ done++;
+ console.log(`[${done}/${toApply.length}] ${pl.handle} ✓ ${verifyOk ? 'verified' : 'VERIFY-FAIL'}`);
+ try {
+ execSync(`node "${path.join(LEDGER_LOG, 'log-exec.mjs')}" ` +
+ `--agent vp-dw-commerce --ticket TK-11073 ` +
+ `--action "Osborne retag ${pl.handle}: color:${pl.colorway || 'none'}, strip AI-palette" ` +
+ `--blast 1 ` +
+ `--undo "cd ${HERE} && node tk11073-rollback.mjs --only=${pl.handle} --apply" ` +
+ `--verify "GET product ${pl.id} — 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}`);
+ console.log(`rollback map: ${ROLLBACK}`);
+}
+main();
diff --git a/scripts/osborne-onboard/tk11073-rollback.mjs b/scripts/osborne-onboard/tk11073-rollback.mjs
new file mode 100644
index 0000000..5ababd5
--- /dev/null
+++ b/scripts/osborne-onboard/tk11073-rollback.mjs
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+/**
+ * TK-11073 rollback — restore products to pre-remediation tags from
+ * ~/.claude/yolo-queue/executed-reversible/TK-11073-rollback-map.json.
+ * Tags-only (Waves 1-2 & 4 are retag-only; no image/PG writes to reverse).
+ *
+ * node tk11073-rollback.mjs # DRY-RUN, all recorded handles
+ * node tk11073-rollback.mjs --only=h1,h2 # DRY-RUN specific handles
+ * node tk11073-rollback.mjs --apply # RESTORE all
+ * node tk11073-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-11073-rollback-map.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();
← 8950edf TK-11073 Wave1: Osborne color: facet from real colorway (tit
·
back to Designerwallcoverings
·
TK-11073 Wave2: Stout color: facet from real color_name, dro 07a4911 →