[object Object]

← back to Designerwallcoverings

TK-11076: PJ retag halted at canary gate (stopped-canary-unsafe) — canary + rollback executor

672d3196781f66331a8d64448e635d15599431a3 · 2026-09-01 12:35:31 -0700 · Steve Abrams

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

Files touched

Diff

commit 672d3196781f66331a8d64448e635d15599431a3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 12:35:31 2026 -0700

    TK-11076: PJ retag halted at canary gate (stopped-canary-unsafe) — canary + rollback executor
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/pj-onboard/out/tk11076-canary-diff.json | 274 ++++++++++++++++++++++++
 scripts/pj-onboard/tk11076-canary.mjs           |  65 ++++++
 scripts/pj-onboard/tk11076-rollback.mjs         |  56 +++++
 3 files changed, 395 insertions(+)

diff --git a/scripts/pj-onboard/out/tk11076-canary-diff.json b/scripts/pj-onboard/out/tk11076-canary-diff.json
new file mode 100644
index 0000000..19e8b1c
--- /dev/null
+++ b/scripts/pj-onboard/out/tk11076-canary-diff.json
@@ -0,0 +1,274 @@
+[
+  {
+    "handle": "dwjp-10101-philip-jeffries-mirage-blue-apparition-on-gesso-vinyl-canvas-new-10486",
+    "status": "ACTIVE",
+    "title": "Mirage-Blue Apparition on Gesso Vinyl Canvas - New",
+    "pattern": "Mirage-Blue Apparition on Gesso Vinyl Canvas",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": "Blue Apparition on Gesso Vinyl Canvas",
+    "source": "pattern dash-tail",
+    "tags_before": [
+      "10486",
+      "Architectural",
+      "Bedroom",
+      "Brown",
+      "browse-hidden",
+      "Chocolate Brown",
+      "Class A Fire Rated",
+      "Commercial",
+      "Cream",
+      "display_variant",
+      "grasscloth",
+      "Grasscloth Texture",
+      "Grasscloth Wallcovering",
+      "Living Room",
+      "Luxury",
+      "Mirage-Blue Apparition on Gesso Vinyl Canvas - NEW",
+      "Natural Wallcovering",
+      "Office",
+      "Organic Modern",
+      "Paper",
+      "Phillip Jeffries",
+      "quotes",
+      "Showroom Line",
+      "Showroom Only",
+      "Solid",
+      "Sophisticated",
+      "Textured",
+      "Timeless",
+      "Traditional",
+      "Vinyl",
+      "Wallcovering",
+      "White"
+    ],
+    "would_strip": [
+      "Brown",
+      "Chocolate Brown",
+      "Cream",
+      "White"
+    ],
+    "legit_color_tags_at_risk": []
+  },
+  {
+    "handle": "hyptonic-hemp-by-phillip-jeffries-at-the-dw-showroom",
+    "status": "ARCHIVED",
+    "title": "Hyptonic Hemp by Phillip Jeffries at the DW Showroom",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": null,
+    "source": "UNRESOLVABLE",
+    "tags_before": [
+      "Beige",
+      "browse-hidden",
+      "Coral",
+      "display_variant",
+      "grasscloth",
+      "Grasscloth Texture",
+      "Grasscloth Wallcovering",
+      "Maroon",
+      "Navy",
+      "Ombre",
+      "Ombre Graasscloth",
+      "Plum",
+      "Salmon",
+      "Showroom Only",
+      "Teal",
+      "Wallcovering"
+    ],
+    "would_strip": [
+      "Beige",
+      "Coral",
+      "Maroon",
+      "Navy",
+      "Plum",
+      "Salmon",
+      "Teal"
+    ],
+    "legit_color_tags_at_risk": []
+  },
+  {
+    "handle": "untethered-by-phillip-jeffries",
+    "status": "ACTIVE",
+    "title": "Untethered - Black  by Phillip Jeffries at the DW Showroom",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": null,
+    "source": "UNRESOLVABLE",
+    "tags_before": [
+      "Abstract",
+      "AI-Analyzed-v2",
+      "Architectural",
+      "Black",
+      "browse-hidden",
+      "Class A Fire Rated",
+      "Cocoa",
+      "Commercial",
+      "Contemporary",
+      "Cream",
+      "Custom",
+      "display_variant",
+      "Gold",
+      "GOLD LEAF",
+      "Luxury",
+      "Organic Modern",
+      "Paper",
+      "Phillip Jeffries",
+      "quotes",
+      "Showroom Line",
+      "Showroom Only",
+      "Untethered",
+      "Wallcovering",
+      "Yellow"
+    ],
+    "would_strip": [
+      "Black",
+      "Cocoa",
+      "Cream",
+      "Gold",
+      "Yellow"
+    ],
+    "legit_color_tags_at_risk": []
+  },
+  {
+    "handle": "dwjp-10003-philip-jeffries-palm-paradise-golden-jungle-new-10563",
+    "status": "ACTIVE",
+    "title": "Palm Paradise-Golden Jungle - New",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": null,
+    "source": "UNRESOLVABLE",
+    "tags_before": [
+      "10563",
+      "Almond",
+      "Architectural",
+      "Bedroom",
+      "browse-hidden",
+      "Camel",
+      "Coastal",
+      "Commercial",
+      "Cream",
+      "display_variant",
+      "Living Room",
+      "Luxury",
+      "Mahogany",
+      "Office",
+      "Palm Paradise-Golden Jungle - NEW",
+      "Paper",
+      "Pattern",
+      "Phillip Jeffries",
+      "Phillip Jeffries Wallcovering",
+      "quotes",
+      "Saddle Brown",
+      "Showroom Line",
+      "Solid",
+      "Sophisticated",
+      "Timeless",
+      "Traditional",
+      "Tropical",
+      "Wallcovering",
+      "White"
+    ],
+    "would_strip": [
+      "Almond",
+      "Camel",
+      "Cream",
+      "Mahogany",
+      "Saddle Brown",
+      "White"
+    ],
+    "legit_color_tags_at_risk": []
+  },
+  {
+    "handle": "dwjp-10007-philip-jeffries-braided-raffia-natural-essence-new-10298",
+    "status": "ACTIVE",
+    "title": "Braided Raffia-Natural Essence - New",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": null,
+    "source": "UNRESOLVABLE",
+    "tags_before": [
+      "Architectural",
+      "Bedroom",
+      "Braided Raffia-Natural Essence - NEW",
+      "Brown",
+      "browse-hidden",
+      "Chocolate Brown",
+      "Commercial",
+      "Cream",
+      "display_variant",
+      "Grasscloth",
+      "Living Room",
+      "Luxury",
+      "Natural Wallcovering",
+      "Office",
+      "Paper",
+      "Phillip Jeffries",
+      "quotes",
+      "Raffia",
+      "Rustic",
+      "Showroom Line",
+      "Showroom Only",
+      "Solid",
+      "Sophisticated",
+      "Textured",
+      "Timeless",
+      "Traditional",
+      "Wallcovering",
+      "White"
+    ],
+    "would_strip": [
+      "Brown",
+      "Chocolate Brown",
+      "Cream",
+      "White"
+    ],
+    "legit_color_tags_at_risk": []
+  },
+  {
+    "handle": "dwjp-10008-philip-jeffries-braided-raffia-arctic-twist-new-10300",
+    "status": "ACTIVE",
+    "title": "Braided Raffia-Arctic Twist - New",
+    "color_name_anchor": "(empty)",
+    "color_primary": "(empty)",
+    "resolved_colorway": null,
+    "source": "UNRESOLVABLE",
+    "tags_before": [
+      "Architectural",
+      "Bedroom",
+      "Braided Raffia-Arctic Twist - NEW",
+      "Brown",
+      "browse-hidden",
+      "Chocolate Brown",
+      "Commercial",
+      "Cream",
+      "display_variant",
+      "Grasscloth",
+      "Living Room",
+      "Luxury",
+      "Natural Wallcovering",
+      "Office",
+      "Paper",
+      "Phillip Jeffries",
+      "quotes",
+      "Raffia",
+      "Rustic",
+      "Showroom Line",
+      "Showroom Only",
+      "Solid",
+      "Sophisticated",
+      "Textured",
+      "Timeless",
+      "Traditional",
+      "Wallcovering",
+      "White"
+    ],
+    "would_strip": [
+      "Brown",
+      "Chocolate Brown",
+      "Cream",
+      "White"
+    ],
+    "legit_color_tags_at_risk": []
+  }
+]
diff --git a/scripts/pj-onboard/tk11076-canary.mjs b/scripts/pj-onboard/tk11076-canary.mjs
new file mode 100644
index 0000000..bf0b23d
--- /dev/null
+++ b/scripts/pj-onboard/tk11076-canary.mjs
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 — Phillip Jeffries CANARY. READ-ONLY. No writes.
+ * For 6 diverse live PJ products, resolve the real colorway from the anchor
+ * pj_catalog.color_name (spec anchor), and — where color_name is empty — from
+ * the Pattern-Colorway dash-tail of pattern_name. Then compute the tag strip
+ * the batch WOULD apply, and JUDGE whether the strip is safe (i.e. whether we
+ * can cleanly separate AI-palette color-word strays from legit descriptive
+ * color tags). PJ has NO per-product AI-palette source (ai_colors /
+ * ai_background_color / ai_tags are 100% NULL), so the only strip rule
+ * available is a GLOBAL bare-color-word denylist — which the osborne/sanderson
+ * remediation explicitly rejected as unsafe.
+ */
+import { execSync } from 'node:child_process';
+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='2024-10';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v){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:q,variables:v})});return (await res.json()).data;}
+const decode=s=>String(s||'').replace(/&amp;/g,'&').replace(/&#39;/g,"'").replace(/&quot;/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();
+
+// pull pj_catalog rows keyed by shopify_product_id (the reliable join)
+function pjMap(){
+  const raw=execSync(`psql "${DB}" -tAc "select coalesce(json_agg(json_build_object('spid',shopify_product_id,'pat',pattern_name,'color',color_name,'cp',color_primary))::text,'[]') from pj_catalog"`,{encoding:'utf8',maxBuffer:128*1024*1024}).trim();
+  const m=new Map();
+  for(const r of JSON.parse(raw||'[]')) if(r.spid) m.set(String(r.spid),r);
+  return m;
+}
+async function main(){
+  const pj=pjMap();
+  // 6 diverse: a clean-color_name product, a dash-tail product, an all-caps GOLD LEAF, an ombre, a grasscloth, an archived
+  const d=await gql(`query{products(first:250, query:"vendor:'Phillip Jeffries'"){nodes{id handle status title tags variants(first:2){nodes{sku}}}}}`);
+  const nodes=d.products.nodes;
+  const withColorName=nodes.find(n=>{const r=pj.get(n.id.split('/').pop());return r&&r.color&&r.color.length<40;});
+  const dashTail=nodes.find(n=>{const r=pj.get(n.id.split('/').pop());return r&&(!r.color)&&r.pat&&r.pat.includes('-');});
+  const heavyPalette=nodes.find(n=>n.tags.filter(t=>/^(black|white|cream|gold|beige|navy|teal|coral|plum|almond|camel|mahogany)$/i.test(t)).length>=4);
+  const pick=[...new Set([withColorName,dashTail,heavyPalette,...nodes].filter(Boolean))].slice(0,6);
+  const report=[];
+  const DENY=new Set(['white','black','grey','gray','ivory','cream','beige','stone','taupe','greige','oatmeal','silver','gold','bronze','copper','pewter','charcoal','slate','teal','aqua','turquoise','jade','sage','green','blue','navy','indigo','cobalt','red','burgundy','coral','salmon','blush','pink','rose','lavender','purple','plum','mauve','yellow','ochre','mustard','amber','peach','tan','walnut','brown','chocolate','khaki','almond','camel','mahogany','cocoa','maroon','saddle brown','chocolate brown']);
+  for(const p of pick){
+    const r=pj.get(p.id.split('/').pop())||{};
+    let colorway=r.color&&r.color.length<40&&!/repeat:|panel|mural|cm\)/i.test(r.color)?r.color:null;
+    let source='color_name';
+    if(!colorway&&r.pat&&r.pat.includes('-')){colorway=r.pat.split('-').slice(1).join('-').trim()||null;source='pattern dash-tail';}
+    if(!colorway)source='UNRESOLVABLE';
+    // simulate strip: remove bare color-word tags NOT equal to colorway; add color:<colorway>
+    const cwNorm=colorway?norm(colorway):null;
+    const wouldStrip=[], legitAtRisk=[];
+    for(const t of p.tags){
+      if(DENY.has(norm(t))&&(!cwNorm||norm(t)!==cwNorm)){
+        wouldStrip.push(t);
+        // Is this stray ACTUALLY a legit color of the product? We have NO per-product palette to tell.
+        // color_primary is the only signal (and it's empty for the affected set).
+        if(r.cp&&norm(t)===norm(r.cp)) legitAtRisk.push(t+' (==color_primary, LEGIT)');
+      }
+    }
+    report.push({handle:p.handle,status:p.status,title:p.title,pattern:r.pat,color_name_anchor:r.color||'(empty)',color_primary:r.cp||'(empty)',resolved_colorway:colorway,source,tags_before:p.tags,would_strip:wouldStrip,legit_color_tags_at_risk:legitAtRisk});
+    await sleep(250);
+  }
+  console.log(JSON.stringify(report,null,2));
+}
+main();
diff --git a/scripts/pj-onboard/tk11076-rollback.mjs b/scripts/pj-onboard/tk11076-rollback.mjs
new file mode 100644
index 0000000..d0b141b
--- /dev/null
+++ b/scripts/pj-onboard/tk11076-rollback.mjs
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 — Phillip Jeffries rollback executor.
+ *
+ * The TK-11076 batch was HALTED at the canary gate (stopped-canary-unsafe): no
+ * product tags were ever modified, so this rollback has nothing to undo. It is
+ * present per the one-file-per-vendor rollback rule and is fully functional: if
+ * a future authorized PJ retag ever populates the rollback map with per-handle
+ * {old_tags} entries, this restores each handle's tags to old_tags.
+ *
+ *   node tk11076-rollback.mjs               # DRY-RUN: list what WOULD be restored
+ *   node tk11076-rollback.mjs --only=h1,h2  # scope to specific handles
+ *   node tk11076-rollback.mjs --apply       # restore old_tags on Shopify
+ */
+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-Phillip_Jeffries-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(q, v) {
+  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: q, variables: v }) });
+  return (await res.json()).data;
+}
+
+async function main() {
+  if (!fs.existsSync(ROLLBACK)) { console.error(`no rollback file: ${ROLLBACK}`); process.exit(1); }
+  const map = JSON.parse(fs.readFileSync(ROLLBACK, 'utf8'));
+  const entries = map.entries || {};
+  let handles = Object.keys(entries);
+  if (ONLY.length) handles = handles.filter(h => ONLY.includes(h));
+  if (!handles.length) {
+    console.log(`Nothing to roll back (status=${map.status}). No per-handle entries — the TK-11076 batch was halted at the canary gate, so no tags were changed.`);
+    return;
+  }
+  for (const h of handles) {
+    const e = entries[h];
+    console.log(`${APPLY ? 'RESTORE' : 'DRY'} ${h} → old_tags(${(e.old_tags || []).length})`);
+    if (APPLY) {
+      const d = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ userErrors{ message } } }`,
+        { input: { id: e.id, tags: e.old_tags } });
+      const ue = d && d.productUpdate && d.productUpdate.userErrors;
+      if (ue && ue.length) console.error(`  userErrors ${h}: ${JSON.stringify(ue)}`);
+      await sleep(500);
+    }
+  }
+}
+main();

← c3c3b07 TK-11073 Wave2: Stout LIVE remediation — 180 products retagg  ·  back to Designerwallcoverings  ·  TK-11076 China Seas retag: canary-first remediate+rollback s ce61a40 →