[object Object]

← back to Designerwallcoverings

TK-11076: colorway-title-anchor PL-exclusion + Malibu run scripts

bfae207fc567cd5ce2e871b7d5cd803d45339ca6 · 2026-09-04 12:42:38 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STupyqBoLfTPHaTr1Gi42b

Files touched

Diff

commit bfae207fc567cd5ce2e871b7d5cd803d45339ca6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 4 12:42:38 2026 -0700

    TK-11076: colorway-title-anchor PL-exclusion + Malibu run scripts
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01STupyqBoLfTPHaTr1Gi42b
---
 .../colorway-title-anchor/out/build-comma-memo.mjs |  27 +++++
 .../colorway-title-anchor/out/run-comma-malibu.sh  |  25 +++++
 .../out/run-remaining-malibu.sh                    |  35 ++++++
 .../colorway-title-anchor/out/set-one-colorway.mjs |  57 ++++++++++
 .../colorway-title-anchor/run-remaining-missing.sh |  37 +++++++
 .../colorway-title-anchor/tk11076-pl-exclusion.mjs | 120 +++++++++++++++++++++
 6 files changed, 301 insertions(+)

diff --git a/scripts/colorway-title-anchor/out/build-comma-memo.mjs b/scripts/colorway-title-anchor/out/build-comma-memo.mjs
new file mode 100644
index 0000000..674bd30
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/build-comma-memo.mjs
@@ -0,0 +1,27 @@
+import fs from 'node:fs';
+import os from 'node:os';
+const rows = fs.readFileSync(new URL('./malibu-comma-handles.txt', import.meta.url), 'utf8')
+  .trim().split('\n').map(l => { const [handle, colorway, ...t] = l.split('\t'); return { handle, colorway, title: t.join('\t') }; });
+const isMal = c => /,\s*and\b/i.test(c);
+const malformed = rows.filter(d => isMal(d.colorway));
+const lists = rows.filter(d => !isMal(d.colorway));
+const esc = s => (s || '').replace(/\|/g, '\\|');
+let m = `# TK-11107 — Malibu Wallpaper colorway retag: ${rows.length} COMMA-colorway products HELD for review\n\n`;
+m += `**Filed:** 2026-09-02 · **Agent:** tk11078-malibu-pilot · **Ticket:** TK-11107\n`;
+m += `**Status:** HELD — excluded from the Steve-approved full apply (they contain commas; the pilot gate excluded comma colorways). NOT written to Shopify.\n\n`;
+m += `## Context\nSteve approved the full Malibu Wallpaper colorway retag. Of 655 would-write products, **610 clean-parse were applied** (title-anchored \`color:\` tag + \`custom.real_color_name\`; titles/prices/status/SKUs/images untouched; reversible + ledgered). These **${rows.length}** have comma-containing parsed colorways and were held.\n\n`;
+m += `## Group A — ${malformed.length} MALFORMED parse artifacts (dangling ", and") — recommend FIX before applying\n`;
+m += `A colorway should not read e.g. \`Silver, And Crimson\` (real title colorway is "Metallic Gold, Silver, and Crimson"). The parser needs to handle the Oxford-comma/list grammar in these titles.\n\n`;
+m += `| handle | parsed_colorway | title |\n|---|---|---|\n`;
+for (const d of malformed) m += `| ${d.handle} | \`${d.colorway}\` | ${esc(d.title)} |\n`;
+m += `\n## Group B — ${lists.length} multi-color LISTS (probably legit) — your call to APPLY as-is\n`;
+m += `Plausibly-real multi-color colorways. If applied, the tag becomes \`color:A / B / C\` (commas→slashes); the metafield keeps the verbatim comma form.\n\n`;
+m += `| handle | parsed_colorway | title |\n|---|---|---|\n`;
+for (const d of lists) m += `| ${d.handle} | \`${d.colorway}\` | ${esc(d.title)} |\n`;
+m += `\n## APPROVE / REVISE / BLOCK\n`;
+m += `- [ ] APPLY GROUP B as-is (${lists.length} multi-color lists) — reversible, ledgered, same script via --only\n`;
+m += `- [ ] FIX GROUP A parser then apply (${malformed.length} malformed) — needs a parse-colorway.mjs tweak for list/Oxford-comma grammar\n`;
+m += `- [ ] BLOCK — leave all ${rows.length} as-is\n`;
+const out = os.homedir() + '/.claude/yolo-queue/pending-approval/TK-11107-malibu-comma-colorways-review.md';
+fs.writeFileSync(out, m);
+console.log(`wrote ${out}\n  total=${rows.length} groupA(malformed)=${malformed.length} groupB(lists)=${lists.length}`);
diff --git a/scripts/colorway-title-anchor/out/run-comma-malibu.sh b/scripts/colorway-title-anchor/out/run-comma-malibu.sh
new file mode 100644
index 0000000..e08c64a
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/run-comma-malibu.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# TK-11107 — Steve-APPROVED ("do it") apply of the 45 held comma-colorways.
+#   - 44 multi-color colorways via the standard retag script (out/malibu-comma44.txt)
+#   - 1 manual set for dwqw-56511 (no-delimiter title) -> "Metallic Gold, Silver, and Crimson"
+# Reversible + ledgered. TITLES/prices/status/SKUs/images untouched.
+set -uo pipefail
+cd "$(dirname "$0")/.."   # -> colorway-title-anchor
+export SHOPIFY_ADMIN_TOKEN="$(grep -E '^SHOPIFY_ADMIN_TOKEN=' ~/Projects/secrets-manager/.env | head -1 | cut -d= -f2- | tr -d '"'"'"'"' )"
+[ -z "${SHOPIFY_ADMIN_TOKEN:-}" ] && { echo "FATAL: no SHOPIFY_ADMIN_TOKEN"; exit 1; }
+export TK_AGENT=tk11107-malibu-full
+
+echo "=== apply 44 multi-color comma colorways ==="
+ONLY=$(paste -sd, out/malibu-comma44.txt)
+OUT=$(node tk11076-colorway-retag.mjs --vendor='Malibu Wallpaper' --apply --only="$ONLY" 2>&1)
+echo "$OUT" | grep -E "^\[[0-9]+/|DONE|VERIFY-FAIL|userErrors" | tail -8
+
+echo ""
+echo "=== manual set: dwqw-56511 -> Metallic Gold, Silver, and Crimson ==="
+node out/set-one-colorway.mjs dwqw-56511-handle "Metallic Gold, Silver, and Crimson" 2>&1 | tail -3
+
+RB=~/.claude/yolo-queue/executed-reversible/TK-11076-Malibu_Wallpaper-rollback.json
+RBN=$(node -e "try{const j=require('$RB');console.log(Object.keys(j).filter(k=>k!=='_meta').length)}catch(e){console.log('?')}")
+echo ""
+echo "===== COMMA-SET COMPLETE. rollback json handle entries now: $RBN (expected ~674 = 629 + 45) ====="
+( cd ~/Projects/ticket-system && tk log TK-11107 "Applied 45 comma-colorways (44 via script + 1 manual dwqw-56511). rollback entries=$RBN." >/dev/null 2>&1 ) || true
diff --git a/scripts/colorway-title-anchor/out/run-remaining-malibu.sh b/scripts/colorway-title-anchor/out/run-remaining-malibu.sh
new file mode 100644
index 0000000..58c8426
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/run-remaining-malibu.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# TK-11107 — Steve-APPROVED full Malibu Wallpaper colorway retag, REMAINING chunks.
+# Clean-parse handles ONLY (chunks ad,ae,af,ag). The 45 comma-colorway handles in
+# out/malibu-comma-handles.txt are DELIBERATELY EXCLUDED and held for Steve review.
+# Idempotent: already-correct products are skipped by the script. Reversible + ledgered.
+set -uo pipefail
+cd "$(dirname "$0")/.."   # -> colorway-title-anchor
+export SHOPIFY_ADMIN_TOKEN="$(grep -E '^SHOPIFY_ADMIN_TOKEN=' ~/Projects/secrets-manager/.env | head -1 | cut -d= -f2- | tr -d '"'"'"'"' )"
+if [ -z "${SHOPIFY_ADMIN_TOKEN:-}" ]; then echo "FATAL: no SHOPIFY_ADMIN_TOKEN in secrets"; exit 1; fi
+
+export TK_AGENT=tk11107-malibu-full
+TOTAL_DONE=0; TOTAL_PASS=0; TOTAL_FAIL=0
+for c in ad ae af ag; do
+  f="out/malibu-chunk-$c"
+  [ -f "$f" ] || { echo "skip missing $f"; continue; }
+  ONLY=$(paste -sd, "$f")
+  n=$(echo "$ONLY" | tr ',' '\n' | grep -c .)
+  echo "=== chunk $c ($n handles) ==="
+  OUT=$(node tk11076-colorway-retag.mjs --vendor='Malibu Wallpaper' --apply --only="$ONLY" 2>&1)
+  echo "$OUT" | grep -E "^\[[0-9]+/|DONE|VERIFY-FAIL|userErrors" | tail -6
+  D=$(echo "$OUT" | grep -oE 'DONE Malibu Wallpaper: [0-9]+' | grep -oE '[0-9]+' | head -1); D=${D:-0}
+  P=$(echo "$OUT" | grep -oE 'verify-pass [0-9]+' | grep -oE '[0-9]+' | head -1); P=${P:-0}
+  Fl=$(echo "$OUT" | grep -oE 'verify-fail [0-9]+' | grep -oE '[0-9]+' | head -1); Fl=${Fl:-0}
+  TOTAL_DONE=$((TOTAL_DONE+D)); TOTAL_PASS=$((TOTAL_PASS+P)); TOTAL_FAIL=$((TOTAL_FAIL+Fl))
+  ( cd ~/Projects/ticket-system && tk log TK-11107 "chunk $c: done $D / pass $P / fail $Fl" >/dev/null 2>&1 ) || true
+done
+
+RB=~/.claude/yolo-queue/executed-reversible/TK-11076-Malibu_Wallpaper-rollback.json
+RBN=$(node -e "try{const j=require('$RB');console.log(Object.keys(j).filter(k=>k!=='_meta').length)}catch(e){console.log('?')}")
+LEDN=$(grep -c 'dwqw-56' ~/.claude/yolo-queue/executed-reversible/ledger.jsonl 2>/dev/null || echo '?')
+echo ""
+echo "===== REMAINING-CHUNKS TOTAL: done $TOTAL_DONE · verify-pass $TOTAL_PASS · verify-fail $TOTAL_FAIL ====="
+echo "rollback json handle entries: $RBN   |   ledger dwqw-56 lines: $LEDN"
+( cd ~/Projects/ticket-system && tk log TK-11107 "REMAINING done. total this run: done $TOTAL_DONE pass $TOTAL_PASS fail $TOTAL_FAIL. rollback entries=$RBN ledger dwqw-56=$LEDN. 45 comma-colorways still HELD for Steve." >/dev/null 2>&1 ) || true
+echo "DONE. If verify-fail>0 above, rollback: node tk11076-rollback.mjs --vendor='Malibu Wallpaper' --only=<bad-handles> --apply"
diff --git a/scripts/colorway-title-anchor/out/set-one-colorway.mjs b/scripts/colorway-title-anchor/out/set-one-colorway.mjs
new file mode 100644
index 0000000..da475fd
--- /dev/null
+++ b/scripts/colorway-title-anchor/out/set-one-colorway.mjs
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+/* Targeted single-product colorway set for a title that has NO deterministic delimiter
+ * (dwqw-56511 — "Confucius Tree Traditional Botanical Metallic Gold, Silver, and Crimson").
+ * Same write shape as tk11076-colorway-retag.mjs: strip color:* facets, add color:<facet>,
+ * set custom.real_color_name, record rollback BEFORE mutating, verify, ledger. TITLE UNTOUCHED.
+ *   node set-one-colorway.mjs <handle> "<Verbatim Colorway>"
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { execSync } from 'node:child_process';
+const HANDLE = process.argv[2];
+const COLORWAY = process.argv[3];
+if (!HANDLE || !COLORWAY) { console.error('usage: set-one-colorway.mjs <handle> "<Colorway>"'); process.exit(1); }
+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';
+if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const NS = 'custom', KEY = 'real_color_name';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const norm = s => String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
+async function gql(query, variables) {
+  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 }) });
+  return (await res.json()).data;
+}
+const facetVal = COLORWAY.replace(/\s*,\s*/g, ' / ');
+const facet = 'color:' + facetVal;
+const run = async () => {
+  const d = await gql(`query($q:String!){ products(first:1,query:$q){ nodes{ id handle title tags m:metafield(namespace:"${NS}",key:"${KEY}"){value} } } }`, { q: `handle:${HANDLE}` });
+  const p = d.products.nodes[0];
+  if (!p) { console.error('product not found:', HANDLE); process.exit(1); }
+  const oldTags = p.tags.slice();
+  const newTags = oldTags.filter(t => !/^color:/i.test(t) && norm(t) !== norm(facet));
+  newTags.push(facet);
+  // rollback record BEFORE mutation
+  const RB = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/TK-11076-Malibu_Wallpaper-rollback.json');
+  const rb = fs.existsSync(RB) ? JSON.parse(fs.readFileSync(RB, 'utf8')) : {};
+  rb[p.handle] = { id: p.id, ts: new Date().toISOString(), old_tags: oldTags, new_tags: newTags,
+    old_metafield: (p.m && p.m.value) || '', new_metafield: COLORWAY, note: 'manual set — no-delimiter title' };
+  fs.writeFileSync(RB, JSON.stringify(rb, null, 2));
+  const u = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ product{id} userErrors{field message} } }`,
+    { input: { id: p.id, tags: newTags, metafields: [{ namespace: NS, key: KEY, type: 'single_line_text_field', value: COLORWAY }] } });
+  const ue = u && u.productUpdate && u.productUpdate.userErrors;
+  if (ue && ue.length) { console.error('userErrors:', JSON.stringify(ue)); process.exit(1); }
+  await sleep(600);
+  const v = await gql(`query($id:ID!){ product(id:$id){ tags m:metafield(namespace:"${NS}",key:"${KEY}"){value} } }`, { id: p.id });
+  const facetOk = v.product.tags.some(t => norm(t) === norm(facet));
+  const metaOk = norm(v.product.m && v.product.m.value) === norm(COLORWAY);
+  console.log(`${p.handle} ${facetOk && metaOk ? '✓' : 'VERIFY-FAIL'} (facetOk=${facetOk} metaOk=${metaOk})  tag="${facet}"  meta="${COLORWAY}"`);
+  try { execSync(`node "${path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/log-exec.mjs')}" ` +
+    `--agent tk11107-malibu-full --ticket TK-11076 --action "manual colorway set ${p.handle}: ${facet}" --blast 1 ` +
+    `--undo "cd ${path.dirname(process.argv[1])}/.. && node tk11076-rollback.mjs --vendor=\\"Malibu Wallpaper\\" --only=${p.handle} --apply" ` +
+    `--verify "GET ${p.id} real_color_name + color: facet == ${COLORWAY}"`, { stdio: 'ignore' }); } catch {}
+  console.log('DONE-ONE');
+};
+run();
diff --git a/scripts/colorway-title-anchor/run-remaining-missing.sh b/scripts/colorway-title-anchor/run-remaining-missing.sh
new file mode 100755
index 0000000..78bd98e
--- /dev/null
+++ b/scripts/colorway-title-anchor/run-remaining-missing.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+# TK-11076 Phase B — run the 28 non-PL addressable vendors with no rollback file yet.
+# Serial (one at a time) so with Kravet still running we stay <= 2 vendors concurrently.
+# 90s gap BETWEEN vendors (bulk-push spacing rule). Each vendor's --apply has its own
+# per-vendor rollback file + per-item verify + ledger (built into the retag script).
+set -u
+cd /Users/macstudio3/Projects/designerwallcoverings/scripts/colorway-title-anchor
+export SHOPIFY_ADMIN_TOKEN=$(grep -E "^SHOPIFY_ADMIN_TOKEN=" ~/Projects/secrets-manager/.env | head -1 | cut -d= -f2- | tr -d '"'\''')
+export SHOPIFY_STORE=designer-laboratory-sandbox.myshopify.com
+export SHOPIFY_API_VERSION=2024-10
+
+VENDORS=(
+  "Pixels" "Donghia" "Daisy Bennett" "Mulberry" "Natural Paint" "Threads"
+  "Versace" "Groundworks" "Lee Jofa" "Steve Abrams Studios" "Thibaut"
+  "Brand McKenzie" "Missoni Wallpaper" "Madagascar Walls" "Ultrasuede"
+  "Retro Walls" "Koroseal" "Designers Guild" "Arte International" "Nobilis"
+  "Traditional Whimsy" "Hand Crafted" "Graduate Collection UK" "Osborne & Little"
+  "Graham & Brown" "Winfield Thybony" "Rebecca Moses" "Tres Tintas"
+)
+
+# Wait for Kravet to be the only other running vendor (never exceed 2). Kravet is expected
+# to be running; these small vendors run one at a time = 2 max. If a second non-Kravet
+# vendor were somehow running we'd wait, but this driver is the only other launcher.
+i=0
+total=${#VENDORS[@]}
+for V in "${VENDORS[@]}"; do
+  i=$((i+1))
+  slug=$(echo "$V" | sed 's/[^A-Za-z0-9]/_/g')
+  echo "=== [$i/$total] APPLY $V ($(date +%H:%M:%S)) ==="
+  node tk11076-colorway-retag.mjs --vendor="$V" --apply > "out/apply-${slug}.log" 2>&1
+  tail -1 "out/apply-${slug}.log"
+  if [ "$i" -lt "$total" ]; then
+    echo "--- 90s inter-vendor gap ---"
+    sleep 90
+  fi
+done
+echo "=== ALL 28 MISSING VENDORS DONE ($(date +%H:%M:%S)) ==="
diff --git a/scripts/colorway-title-anchor/tk11076-pl-exclusion.mjs b/scripts/colorway-title-anchor/tk11076-pl-exclusion.mjs
new file mode 100644
index 0000000..84499b0
--- /dev/null
+++ b/scripts/colorway-title-anchor/tk11076-pl-exclusion.mjs
@@ -0,0 +1,120 @@
+#!/usr/bin/env node
+/**
+ * TK-11076 STEP 1 — PRIVATE-LABEL EXCLUSION (authoritative, deterministic, no guessing).
+ *
+ * Steve's hard scope constraint: run the colorway retag ONLY on lines we do NOT private label.
+ * EXCLUDE every private-label line entirely. If a vendor's PL status is AMBIGUOUS -> EXCLUDE
+ * (conservative) and list it for Steve.
+ *
+ * Sources of truth (in priority order):
+ *   1. vendor_registry.is_private_label / private_label_name  (the authoritative flag)
+ *   2. The customer-facing private_label_name values (Phillipe Romano, Malibu Wallpaper, Hollywood, etc.)
+ *      — because shopify_products.vendor holds the CUSTOMER-FACING string.
+ *   3. Steve's explicit named directives that OVERRIDE registry drift:
+ *        - Greenland (cork, sold as Phillipe Romano; "Greenland" must never surface) — registry has it
+ *          flagged false = DRIFT, force-exclude.
+ *        - The Phillipe Romano umbrella source lines (astek/command54/rigo/justindavid/greenland/
+ *          relativity/twil/mdc) — all sold under "Phillipe Romano".
+ *   4. CONSERVATIVE catch: any addressable vendor whose live name is a DW house/private brand
+ *      (not a real outside manufacturer) and whose PL status is not cleanly resolvable -> EXCLUDE + flag.
+ *
+ * Output: out/tk11076-exclusion.json  { excluded:[{vendor,count,reason}], clean:[{vendor,write}], ... }
+ */
+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 OUT = path.join(HERE, 'out');
+const DB = 'postgresql:///dw_unified?host=/tmp';
+const addr = JSON.parse(fs.readFileSync(path.join(OUT, 'tk11076-addressable-by-vendor.json'), 'utf8'));
+
+const norm = s => String(s || '').toLowerCase().replace(/&amp;/g, '&').replace(/\s*&\s*/g, ' & ')
+  .replace(/[^a-z0-9& ]/g, ' ').replace(/\s+/g, ' ').trim();
+
+// ---- 1+2: registry-driven PL universe (source vendor_name UNION customer-facing private_label_name)
+const regRows = execSync(`psql "${DB}" -tAF '|' -c "` +
+  `SELECT vendor_name, COALESCE(private_label_name,''), is_private_label FROM vendor_registry"`,
+  { encoding: 'utf8' }).trim().split('\n').map(l => l.split('|'));
+
+const plNames = new Set();      // normalized names that are private-label
+const plReason = new Map();     // norm-name -> human reason
+function addPL(name, reason) { const n = norm(name); if (n) { plNames.add(n); if (!plReason.has(n)) plReason.set(n, reason); } }
+
+for (const [vname, plname, ispl] of regRows) {
+  if (ispl === 't') {
+    addPL(vname, `registry is_private_label=true (source line)`);
+    if (plname && plname.trim()) addPL(plname, `registry customer-facing PL brand "${plname}"`);
+  }
+}
+
+// ---- 3: explicit named overrides (Steve's directive + cheat sheet) — force-exclude regardless of registry flag
+const FORCE_EXCLUDE = [
+  ['Greenland', 'Steve directive: Greenland cork sold as Phillipe Romano; name must never surface (registry flag=false is DRIFT)'],
+  ['Greenland Printing', 'Steve directive: Greenland → Phillipe Romano'],
+  ['Los Angeles Fabrics', 'cheat sheet: Justin David fabrics, customer-facing "Los Angeles Fabrics", under Phillipe Romano umbrella (PL)'],
+  ['LA Fabrics', 'PL: Justin David / Phillipe Romano fabric line'],
+];
+for (const [n, r] of FORCE_EXCLUDE) addPL(n, r);
+
+// ---- 4: conservative DW-house-brand catch. Live vendor names that are DW OWN brands (not outside
+// manufacturers) get excluded even if not registry-flagged, and are FLAGGED as ambiguous for Steve.
+// These are matched against the addressable set below; a name qualifies if it is clearly a DW house/
+// private label per its wording. We keep this list explicit (no fuzzy manufacturer over-exclusion).
+const AMBIGUOUS_DW_HOUSE = new Map([
+  ['designer wallcoverings', 'DW house brand (our own label) — is_private_label context; exclude conservatively'],
+  ['dw home', 'DW house brand'],
+  ['dw bespoke studio', 'DW house brand (registry PL)'],
+  ['glass beaded', 'DW house-sourced brand (Glass Bead is registry PL) — exclude conservatively'],
+  ['glass bead', 'DW house-sourced brand (registry PL)'],
+  ['glambeads by dw', 'DW house brand ("by DW")'],
+  ['glitter walls', 'DW multi-source house brand (registry PL)'],
+  ['surface stick', 'DW house/sourced brand — PL status unresolved, exclude conservatively + flag'],
+  ['la walls', 'DW house brand (LA Walls) — PL status unresolved, exclude conservatively + flag'],
+  ['architectural fabrics', 'DW "Architectural" house rebrand line — exclude conservatively + flag'],
+  ['hollywood acoustical', 'Hollywood (registry PL: Momentum/Versa private-labeled as Hollywood)'],
+  ['hollywood wallcoverings', 'registry PL (Momentum/Versa → Hollywood)'],
+  ['los angeles fabrics', 'Justin David / Phillipe Romano (PL)'],
+  ['malibu', 'collapses to the excluded PL brand "Malibu Wallpaper" (WallQuest/Brewster private label) — exclude conservatively'],
+]);
+for (const [n, r] of AMBIGUOUS_DW_HOUSE) { if (!plNames.has(n)) { plNames.add(n); plReason.set(n, r + ' [AMBIGUOUS→EXCLUDE]'); } }
+
+// ---- intersect with the addressable set
+const excluded = [], clean = [], flaggedAmbiguous = [];
+for (const row of addr.per_vendor) {
+  if (row.write === 0) continue;                 // no confident writes anyway; not in the write set
+  const n = norm(row.vendor);
+  if (plNames.has(n)) {
+    const reason = plReason.get(n);
+    excluded.push({ vendor: row.vendor, write_excluded: row.write, reason });
+    if (/AMBIGUOUS/.test(reason)) flaggedAmbiguous.push({ vendor: row.vendor, write_excluded: row.write, reason });
+  } else {
+    clean.push({ vendor: row.vendor, write: row.write });
+  }
+}
+
+excluded.sort((a, b) => b.write_excluded - a.write_excluded);
+clean.sort((a, b) => b.write - a.write);
+
+const out = {
+  ts: new Date().toISOString(),
+  ticket: 'TK-11076',
+  addressable_write_total: addr.write,
+  excluded_private_label: excluded,
+  excluded_write_count: excluded.reduce((s, r) => s + r.write_excluded, 0),
+  flagged_ambiguous_for_steve: flaggedAmbiguous,
+  clean_non_private_label: clean,
+  clean_write_count: clean.reduce((s, r) => s + r.write, 0),
+};
+fs.writeFileSync(path.join(OUT, 'tk11076-exclusion.json'), JSON.stringify(out, null, 2));
+
+console.log(`\n=== TK-11076 STEP 1: PRIVATE-LABEL EXCLUSION ===`);
+console.log(`addressable write total: ${addr.write}`);
+console.log(`\n--- EXCLUDED (private-label) : ${excluded.length} vendors, ${out.excluded_write_count} writes ---`);
+for (const r of excluded) console.log(`  EXCLUDE ${String(r.write_excluded).padStart(4)}  ${r.vendor.padEnd(28)} — ${r.reason}`);
+console.log(`\n--- FLAGGED AMBIGUOUS (excluded conservatively, surface to Steve) : ${flaggedAmbiguous.length} ---`);
+for (const r of flaggedAmbiguous) console.log(`  FLAG    ${String(r.write_excluded).padStart(4)}  ${r.vendor.padEnd(28)} — ${r.reason}`);
+console.log(`\n--- CLEAN (non-private-label, eligible to WRITE) : ${clean.length} vendors, ${out.clean_write_count} writes ---`);
+for (const r of clean) console.log(`  WRITE   ${String(r.write).padStart(4)}  ${r.vendor}`);
+console.log(`\nexclusion JSON: ${path.join(OUT, 'tk11076-exclusion.json')}`);

← 7c24b65 TK-11076: Cole & Son color-facet remediation + rollback scri  ·  back to Designerwallcoverings  ·  TK-11076: Hollywood rollback artifact (no-op — batch judged 86ca8d4 →