[object Object]

← back to Dw Validator Debug TK11314

snapshot before fixing roll price validation

59499ae19d72e825922b4fbfb183fdd825624bc0 · 2026-08-27 17:05:09 -0700 · Steve

Files touched

Diff

commit 59499ae19d72e825922b4fbfb183fdd825624bc0
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 27 17:05:09 2026 -0700

    snapshot before fixing roll price validation
---
 DW-Agents/vendor-command-center/server.js          |   2 +-
 .../scrapers/glitter-walls-new-products-scraper.ts |   6 +-
 .../lib/scrapers/malibu-new-products-scraper.ts    |   6 +-
 .../scripts/run-all-vendor-scrapers.ts             | 229 ++++++++++++++++++++-
 DW-Programming/anna-french-shopify-push.js         |  53 +++++
 DW-Programming/command54-shopify-push.js           |  73 +++++++
 DW-Programming/fabricut-daily-post.js              |  67 +++++-
 DW-Programming/fabricut-shopify-update.js          | Bin 0 -> 11680 bytes
 DW-Programming/hollywood-shopify-push.js           |  77 +++++++
 DW-Programming/justindavid-shopify-push.js         |  70 +++++++
 DW-Programming/push-maya-romanoff-to-shopify.js    |  70 +++++++
 mailers/assets/pj-fall-2026/probe.html             | 124 +++++++++++
 shopify/scripts/maharam-gmc-unpublish.js           | 114 ++++++++++
 .../ROLLBACK-republish-2026-08-26T19-28-08-055Z.js |   9 +
 .../ROLLBACK-republish-2026-08-26T19-36-35-341Z.js |   9 +
 .../ROLLBACK-republish-2026-08-26T19-45-53-920Z.js |   9 +
 shopify/scripts/maharam-price-load.js              | 113 ++++++++++
 shopify/scripts/zoffany-work/zof-recheck.mjs       |   7 +
 shopify/scripts/zoffany-work/zof-repair.mjs        |  22 ++
 shopify/scripts/zoffany-work/zof-reprice.mjs       |  56 +++++
 shopify/scripts/zoffany-work/zof-sample-run.mjs    |  44 ++++
 shopify/scripts/zoffany-work/zof-vp.mjs            |   5 +
 22 files changed, 1149 insertions(+), 16 deletions(-)

diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index 8405ca9c..5202e540 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -6094,7 +6094,7 @@ th[data-sort].sort-asc, th[data-sort].sort-desc { color: var(--accent); }
       <span class="stat-value" id="statTotalProducts">--</span>
       <span class="stat-label">Catalog (DB)</span>
     </div>
-    <div class="stat-card" style="border-color:#60a5fa" onclick="openStatModal('shopify','On Shopify','shopify_count')">
+    <div class="stat-card" style="border-color:#60a5fa" onclick="openStatModal('shopify','On Shopify','shopify_count')" title="Products attributed to a registered vendor (VENDOR_NAME_MAP), excluding DELETED_FROM_SHOPIFY + DUPLICATE_IMPORT. This is a per-vendor rollup, NOT the raw shopify_products row count.">
       <span class="stat-value" id="statShopifyCount" style="color:#60a5fa">--</span>
       <span class="stat-label">On Shopify</span>
     </div>
diff --git a/DW-Programming/ImportNewSkufromURL/lib/scrapers/glitter-walls-new-products-scraper.ts b/DW-Programming/ImportNewSkufromURL/lib/scrapers/glitter-walls-new-products-scraper.ts
index 0c56e8eb..8e5fe7aa 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/scrapers/glitter-walls-new-products-scraper.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/scrapers/glitter-walls-new-products-scraper.ts
@@ -74,8 +74,10 @@ export async function scrapeGlitterWallsNewProducts(params?: ScrapeParams) {
 }
 
 export default async function scrapeGlitterWallsNewProductsScraper(params: ScrapeParams) {
-  const products = await scrapeGlitterWallsNewProducts(params);
-  return { vendor: 'glitter-walls', url: params?.url, maxResults: params?.maxResults, products };
+  // TK-10789: return the products array directly so run-all-vendor-scrapers.ts
+  // Array.isArray() check passes. The wrapper object shape caused "No products returned"
+  // even when the feed returned products correctly.
+  return await scrapeGlitterWallsNewProducts(params);
 }
 
 export { scrapeGlitterWallsNewProductsScraper };
diff --git a/DW-Programming/ImportNewSkufromURL/lib/scrapers/malibu-new-products-scraper.ts b/DW-Programming/ImportNewSkufromURL/lib/scrapers/malibu-new-products-scraper.ts
index f8fc1bd0..38893bee 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/scrapers/malibu-new-products-scraper.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/scrapers/malibu-new-products-scraper.ts
@@ -76,8 +76,10 @@ export async function scrapeMalibuNewProducts(params?: ScrapeParams) {
 }
 
 export default async function scrapeMalibuNewProductsScraper(params: ScrapeParams) {
-  const products = await scrapeMalibuNewProducts(params);
-  return { vendor: 'malibu', url: params?.url, maxResults: params?.maxResults, products };
+  // TK-10789: return the products array directly (same fix as glitter-walls) so
+  // run-all-vendor-scrapers.ts Array.isArray() check passes. Wrapper object caused
+  // "No products returned" if this vendor ID ever appears in the monthly sweep.
+  return await scrapeMalibuNewProducts(params);
 }
 
 export { scrapeMalibuNewProductsScraper };
diff --git a/DW-Programming/ImportNewSkufromURL/scripts/run-all-vendor-scrapers.ts b/DW-Programming/ImportNewSkufromURL/scripts/run-all-vendor-scrapers.ts
index f28888e0..c02c3187 100644
--- a/DW-Programming/ImportNewSkufromURL/scripts/run-all-vendor-scrapers.ts
+++ b/DW-Programming/ImportNewSkufromURL/scripts/run-all-vendor-scrapers.ts
@@ -76,6 +76,194 @@ function resolveVendorList(): string[] {
   return verified.filter((id) => hashDay(id) === day);
 }
 
+// ── Per-vendor _catalog RETARGET (TK-10756) ──────────────────────────────────
+// The sweep's output must land in the matching per-vendor `<vendor>_catalog`
+// staging table (the canonical table the onboarding/cadence scripts read), NOT
+// only the generic `products` dead-end. Mapping is resolved DETERMINISTICALLY
+// off vendor_registry.catalog_table (never a fuzzy name guess), and the target
+// table's REAL columns + REAL single-column unique key are introspected at
+// runtime so we upsert on the table's true key and only touch columns it has.
+// STAGE-ONLY: this writes staging `_catalog` tables exclusively — zero live-store
+// contact (the runtime tripwire in the wrapper stays intact).
+
+type CatalogTarget = {
+  table: string;        // e.g. "arte_catalog"
+  keyCol: string;       // real single-column unique/PK, e.g. "arte_sku" | "mfr_sku"
+  columns: Set<string>; // real column set of the target table
+};
+
+// resolved-per-vendor cache for this run (short-lived process → run-scoped cache is safe)
+const catalogTargetCache = new Map<string, CatalogTarget | null>();
+
+// Quote a Postgres identifier safely (defends against poisoned registry values /
+// reserved words / mixed case). Values always stay parameterized ($1..$n).
+function quoteIdent(id: string): string {
+  return '"' + String(id).replace(/"/g, '""') + '"';
+}
+
+// Resolve vendorId → { catalog_table } via vendor_registry.catalog_table ONLY.
+// Match vendor_code exactly, then vendorId with hyphens→underscores. If no row,
+// or catalog_table is empty, or it is the generic shared `vendor_catalog`
+// (no vendor-specific identity + no unique key), return null → caller SKIPS + logs.
+// We NEVER invent a table name.
+async function resolveCatalogTarget(vendorId: string): Promise<CatalogTarget | null> {
+  if (catalogTargetCache.has(vendorId)) return catalogTargetCache.get(vendorId)!;
+
+  let table: string | null = null;
+  try {
+    const norm = vendorId.replace(/-/g, "_");
+    const reg = await pool.query(
+      `SELECT catalog_table FROM vendor_registry
+        WHERE vendor_code = $1 OR vendor_code = $2
+        ORDER BY (vendor_code = $1) DESC, id ASC
+        LIMIT 1`,
+      [vendorId, norm]
+    );
+    const ct = (reg.rows[0]?.catalog_table || "").trim();
+    if (!ct) {
+      console.log(`   ↪ _catalog SKIP (${vendorId}): no catalog_table in vendor_registry`);
+      catalogTargetCache.set(vendorId, null);
+      return null;
+    }
+    if (ct === "vendor_catalog") {
+      // generic shared staging — no per-vendor identity, no enforced unique key →
+      // upserting here would misroute rows into a wrong/keyless table. Skip.
+      console.log(`   ↪ _catalog SKIP (${vendorId}): registered to generic vendor_catalog (no unique key) — leaving to native pipeline`);
+      catalogTargetCache.set(vendorId, null);
+      return null;
+    }
+    table = ct;
+  } catch (e: any) {
+    console.log(`   ↪ _catalog SKIP (${vendorId}): registry lookup error — ${e.message?.substring(0, 60)}`);
+    catalogTargetCache.set(vendorId, null);
+    return null;
+  }
+
+  // Confirm the table exists.
+  try {
+    const exists = await pool.query(`SELECT to_regclass('public.' || $1) AS oid`, [table]);
+    if (!exists.rows[0]?.oid) {
+      console.log(`   ↪ _catalog SKIP (${vendorId}): registered table ${table} does not exist`);
+      catalogTargetCache.set(vendorId, null);
+      return null;
+    }
+  } catch {
+    catalogTargetCache.set(vendorId, null);
+    return null;
+  }
+
+  // Introspect the real column set.
+  const colRes = await pool.query(
+    `SELECT column_name FROM information_schema.columns
+      WHERE table_schema='public' AND table_name=$1`,
+    [table]
+  );
+  const columns = new Set<string>(colRes.rows.map((r: any) => r.column_name));
+
+  // Find the REAL single-column UNIQUE (or PK) key from constraints — NOT just a
+  // column that happens to exist. This is what makes arte_catalog resolve to its
+  // real key `arte_sku` (it has an mfr_sku COLUMN but the unique constraint is on
+  // arte_sku), and makes any keyless table resolve to null → skip.
+  const keyRes = await pool.query(
+    `SELECT MIN(kcu.column_name) AS column_name
+       FROM information_schema.table_constraints tc
+       JOIN information_schema.key_column_usage kcu
+         ON tc.constraint_name = kcu.constraint_name
+        AND tc.table_schema = kcu.table_schema
+      WHERE tc.table_schema='public' AND tc.table_name=$1
+        AND tc.constraint_type IN ('UNIQUE','PRIMARY KEY')
+      GROUP BY tc.constraint_name
+      HAVING COUNT(kcu.column_name) = 1`,
+    [table]
+  );
+  // Map lower(colname) → real colname so priority match is case-insensitive
+  // (defends against a quoted mixed-case identifier in a future table).
+  const uniqueByLower = new Map<string, string>();
+  for (const r of keyRes.rows) uniqueByLower.set(String(r.column_name).toLowerCase(), r.column_name);
+  // Prefer a natural SKU key; ignore surrogate `id` PKs (never upsert on a serial id).
+  const KEY_PRIORITY = ["mfr_sku", "arte_sku", "vendor_sku", "sku"];
+  let keyCol: string | null = null;
+  for (const c of KEY_PRIORITY) {
+    if (uniqueByLower.has(c)) { keyCol = uniqueByLower.get(c)!; break; }
+  }
+  if (!keyCol) {
+    console.log(`   ↪ _catalog SKIP (${vendorId}): ${table} has no single-column SKU unique key — cannot upsert safely`);
+    catalogTargetCache.set(vendorId, null);
+    return null;
+  }
+
+  const target: CatalogTarget = { table, keyCol, columns };
+  catalogTargetCache.set(vendorId, target);
+  console.log(`   ↪ _catalog target (${vendorId}): ${table} ON CONFLICT(${keyCol})`);
+  return target;
+}
+
+// Upsert one scraped product into its per-vendor _catalog table (primary).
+// imageUrl = the ORIGINAL source image URL (a real URL, not a base64 data URI) —
+// base64 blobs belong in the legacy products.image_data monitor column, never in
+// the canonical image_url. Idempotent: ON CONFLICT(keyCol) DO UPDATE.
+async function upsertCatalog(
+  vendorId: string,
+  sku: string,
+  name: string,
+  color: string,
+  url: string,
+  imageUrl: string | null
+): Promise<boolean> {
+  const target = await resolveCatalogTarget(vendorId);
+  if (!target) return false; // skipped + logged in resolver
+  if (!sku) return false;
+
+  // Candidate field→value map, filtered to columns the target actually has.
+  const candidate: Record<string, any> = {
+    [target.keyCol]: sku,
+    pattern_name: name,
+    color_name: color,
+    product_url: url || null,
+    image_url: imageUrl || null,
+    updated_at: null, // marker → NOW()
+  };
+  const cols: string[] = [];
+  const vals: any[] = [];
+  const placeholders: string[] = [];
+  for (const [c, v] of Object.entries(candidate)) {
+    if (!target.columns.has(c)) continue;
+    cols.push(c);
+    if (c === "updated_at") { placeholders.push("NOW()"); }
+    else { vals.push(v); placeholders.push("$" + vals.length); }
+  }
+  if (!cols.includes(target.keyCol)) return false;
+
+  // Update every mapped col except the conflict key on conflict.
+  const updates = cols
+    .filter((c) => c !== target.keyCol)
+    .map((c) => c === "updated_at"
+      ? `${quoteIdent(c)} = NOW()`
+      : `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
+
+  // If the ONLY mapped column is the key (a degenerate table with no
+  // pattern/color/url/image column), still INSERT the new SKU — just DO NOTHING
+  // on conflict. Never conflate "nothing to update" with "drop the row".
+  const conflictClause = updates.length === 0
+    ? `ON CONFLICT (${quoteIdent(target.keyCol)}) DO NOTHING`
+    : `ON CONFLICT (${quoteIdent(target.keyCol)}) DO UPDATE SET ${updates.join(", ")}`;
+
+  const q =
+    `INSERT INTO ${quoteIdent(target.table)} (${cols.map(quoteIdent).join(", ")}) ` +
+    `VALUES (${placeholders.join(", ")}) ` +
+    conflictClause;
+
+  try {
+    await pool.query(q, vals);
+    return true;
+  } catch (e: any) {
+    // Self-heal on schema drift: invalidate cache so a rename/alter mid-run retries fresh next call.
+    if (/column|does not exist|undefined/i.test(e.message || "")) catalogTargetCache.delete(vendorId);
+    console.log(`   ↪ _catalog upsert error (${vendorId}/${target.table}): ${e.message?.substring(0, 70)}`);
+    return false;
+  }
+}
+
 async function downloadImage(url: string, referer: string): Promise<string | null> {
   if (!url || url.startsWith("data:")) return null;
   try {
@@ -105,8 +293,18 @@ async function upsertProduct(
   name: string,
   color: string,
   url: string,
-  imageData: string | null
+  imageData: string | null,
+  imageUrl: string | null
 ): Promise<boolean> {
+  // PRIMARY (TK-10756): land the row in the vendor's canonical `<vendor>_catalog`
+  // table. imageUrl is the ORIGINAL source URL (a real URL) — NOT the base64
+  // preview blob, which stays in the legacy products.image_data column below.
+  const catalogOk = await upsertCatalog(vendorId, sku, name, color, url, imageUrl);
+
+  // SECONDARY (best-effort): keep the legacy products/vendors staging rows so the
+  // existing ralph-wiggum monitor UI (reads `products`, previews image_data) keeps
+  // working. A failure here does NOT fail the row if the canonical _catalog landed.
+  let legacyOk = false;
   try {
     // Ensure vendor exists
     await pool.query(
@@ -139,11 +337,13 @@ async function upsertProduct(
         [vendorId, sku, name, color, url]
       );
     }
-    return true;
+    legacyOk = true;
   } catch (e: any) {
-    console.log(`   DB Error: ${e.message?.substring(0, 40)}`);
-    return false;
+    console.log(`   DB Error (legacy products/${vendorId}): ${e.message?.substring(0, 40)}`);
   }
+
+  // Row is "saved" if the canonical _catalog OR the legacy staging landed it.
+  return catalogOk || legacyOk;
 }
 
 async function logTestResult(vendorId: string, success: boolean, productCount: number, errorMessage?: string) {
@@ -176,9 +376,15 @@ async function runVendorScraper(vendorId: string): Promise<{ success: boolean; c
     }
 
     console.log(`   📦 Running scraper...`);
-    const products = await scrapeFn({ maxResults: 10 });
-
-    if (!Array.isArray(products) || products.length === 0) {
+    const rawResult = await scrapeFn({ maxResults: 10 });
+    // TK-10789: handle both return shapes — plain array (legacy) and
+    // { vendor, products: [...] } wrapper (54+ newer scrapers) so neither
+    // shape triggers the false "No products returned" error.
+    const products: any[] = Array.isArray(rawResult)
+      ? rawResult
+      : (Array.isArray(rawResult?.products) ? rawResult.products : []);
+
+    if (products.length === 0) {
       console.log(`   ⚠️ No products returned`);
       await logTestResult(vendorId, false, 0, "No products returned");
       return { success: false, count: 0 };
@@ -189,16 +395,19 @@ async function runVendorScraper(vendorId: string): Promise<{ success: boolean; c
 
     for (const p of products) {
       const sku = p.sku || p.code || `${vendorId.toUpperCase()}-${savedCount + 1}`;
-      const name = p.name || p.title || "Unknown";
+      // DW fallback order for pattern_name: pattern/title → MFR SKU → color.
+      // NEVER stage the literal "Unknown" (standing rule).
+      const name = p.name || p.title || sku || p.color || p.colors?.[0]?.name || "";
       const color = p.color || p.colors?.[0]?.name || "Default";
       const url = p.url || p.href || "";
+      const imageUrl: string | null = p.image || null; // ORIGINAL source URL → _catalog.image_url
 
       let imageData: string | null = null;
       if (p.image) {
         imageData = await downloadImage(p.image, url || `https://www.${vendorId}.com`);
       }
 
-      const success = await upsertProduct(vendorId, sku, name, color, url, imageData);
+      const success = await upsertProduct(vendorId, sku, name, color, url, imageData, imageUrl);
       if (success) {
         savedCount++;
         if (imageData) imageCount++;
@@ -258,7 +467,7 @@ async function run() {
   console.log(`Successful Vendors: ${successfulVendors}`);
   console.log(`Total Products Added: ${totalProducts}`);
 
-  console.log("\n📦 Top Vendors by Product Count:");
+  console.log("\n📦 Top Vendors by Product Count (legacy products staging):");
   for (const r of stats.rows) {
     console.log(`   ${r.vendor_id}: ${r.total} products (${r.with_images} with images)`);
   }
diff --git a/DW-Programming/anna-french-shopify-push.js b/DW-Programming/anna-french-shopify-push.js
index 27f75ee6..1d651e40 100644
--- a/DW-Programming/anna-french-shopify-push.js
+++ b/DW-Programming/anna-french-shopify-push.js
@@ -16,6 +16,7 @@
 
 const { Pool } = require('pg');
 const https = require('https');
+const fs = require('fs');
 
 const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
 const SHOPIFY_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
@@ -243,6 +244,9 @@ function buildMetafields(row) {
   return mf;
 }
 
+// Shopify derives a handle from the title (lowercase, non-alphanumerics→'-', collapse/trim).
+function shopifyHandle(title) { return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
+
 async function createProduct(row, dryRun) {
   const patternTitle = toTitleCase(row.pattern_name);
   const colorTitle = toTitleCase(row.color_name);
@@ -254,6 +258,27 @@ async function createProduct(row, dryRun) {
     return { sku: row.mfr_sku, status: 'skipped', reason: 'Unknown in title' };
   }
 
+  // TK-10815 IDEMPOTENCY GUARD (Fabricut-class): never re-create a product whose handle already
+  // exists in Shopify. The shopify_product_id IS NULL query is not enough — a stale flag, a partial
+  // prior run, or two overlapping runs will POST the same handle twice and Shopify appends -1/-2…
+  // (the 2026-08-19 Fabricut double-fire that created 90 clone DRAFTs). Fail CLOSED — a lookup error
+  // propagates (never fall through to POST on an ambiguous "not found"). If the handle is already
+  // live, reconcile the DB and skip.
+  const handle = shopifyHandle(title);
+  const existing = await shopifyRestWithRetry('GET', `/products.json?handle=${encodeURIComponent(handle)}&fields=id,handle`);
+  const hit = existing && existing.products && existing.products.find(p => p.handle === handle);
+  if (hit) {
+    stats.skipped++;
+    if (!dryRun) {
+      await pool.query(
+        'UPDATE anna_french_catalog SET shopify_product_id = $1, updated_at = NOW() WHERE mfr_sku = $2',
+        [hit.id.toString(), row.mfr_sku]
+      );
+    }
+    console.log(`  ↺ ${row.mfr_sku} handle '${handle}' already in Shopify → ${dryRun ? 'would reconcile' : 'reconciled'} shopify_product_id, no dup created`);
+    return { sku: row.mfr_sku, status: 'skipped_exists', shopifyId: hit.id, handle };
+  }
+
   const tags = buildTags(row);
   const bodyHtml = buildDescription(row);
   const metafields = buildMetafields(row);
@@ -334,6 +359,25 @@ async function sendSlack(msg) {
   });
 }
 
+// TK-10815 concurrency lock (Fabricut-class). The handle-exists guard is check-then-act and NOT
+// atomic: two OVERLAPPING live runs both GET-miss the handle, then both POST, re-creating the same
+// -N clones. A single-writer lockfile makes a second live run fail fast. Stale locks (crashed prior
+// run) are stolen via PID-liveness.
+const LOCK = '/tmp/anna-french-shopify-push.lock';
+function acquireLock() {
+  for (let attempt = 0; attempt < 2; attempt++) {
+    try { const fd = fs.openSync(LOCK, 'wx'); fs.writeSync(fd, String(process.pid)); return fd; }
+    catch (e) {
+      if (e.code !== 'EEXIST') throw e;
+      let old = NaN; try { old = parseInt(fs.readFileSync(LOCK, 'utf8'), 10); } catch {}
+      let alive = false; try { if (old) { process.kill(old, 0); alive = true; } } catch { alive = false; }
+      if (alive) { console.error(`FATAL: another anna-french-shopify-push (pid ${old}) holds ${LOCK}. Refusing to double-fire.`); process.exit(2); }
+      try { fs.unlinkSync(LOCK); } catch {}   // stale lock — steal and retry once
+    }
+  }
+  throw new Error('could not acquire lock');
+}
+
 async function main() {
   const args = process.argv.slice(2);
   const dryRun = args.includes('--dry-run');
@@ -355,6 +399,15 @@ async function main() {
   const limitIdx = args.indexOf('--limit');
   if (limitIdx >= 0 && args[limitIdx + 1]) limit = parseInt(args[limitIdx + 1]);
 
+  let lockFd;
+  if (!dryRun) {
+    lockFd = acquireLock();
+    const release = () => { try { if (lockFd !== undefined) fs.closeSync(lockFd); fs.unlinkSync(LOCK); } catch {} };
+    process.on('exit', release);
+    process.on('SIGINT', () => { release(); process.exit(130); });
+    process.on('SIGTERM', () => { release(); process.exit(143); });
+  }
+
   console.log(`=== Anna French Shopify Push (Phase 8) ===${dryRun ? ' [DRY RUN]' : ''}`);
   console.log(`Started: ${new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' })} PT\n`);
 
diff --git a/DW-Programming/command54-shopify-push.js b/DW-Programming/command54-shopify-push.js
index ed4b849f..d43835b4 100644
--- a/DW-Programming/command54-shopify-push.js
+++ b/DW-Programming/command54-shopify-push.js
@@ -23,6 +23,7 @@
 
 const { Pool } = require('pg');
 const https = require('https');
+const fs = require('fs');
 
 const pool = new Pool({
   connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
@@ -51,6 +52,23 @@ function shopifyGql(query, variables) {
     req.write(data); req.end();
   });
 }
+// Shopify derives a handle from the title (lowercase, non-alphanumerics→'-', collapse/trim).
+function shopifyHandle(title) { return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
+// REST GET (fail-closed): rejects on transport/HTTP error so the caller never treats a lookup
+// failure as "handle absent". Verified Shopify 2024-10 REST honors ?handle=. Reads only (id/handle)
+// — no public text touched, so no private-label leak risk.
+function shopifyRestGet(path) {
+  return new Promise((resolve, reject) => {
+    const req = https.request({ host: SHOPIFY_STORE, path: `/admin/api/${SHOPIFY_API}${path}`, method: 'GET',
+      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' }, timeout: 30000 },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => {
+        if (r.statusCode >= 400) { reject(new Error(`Shopify GET ${r.statusCode}: ${d.slice(0, 200)}`)); return; }
+        try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`Shopify GET parse error: ${e.message}`)); } }); });
+    req.on('error', reject);
+    req.on('timeout', () => { req.destroy(); reject(new Error('Shopify GET timeout')); });
+    req.end();
+  });
+}
 let _PUBS = null;
 async function _loadPubs() {
   if (_PUBS) return _PUBS;
@@ -424,7 +442,34 @@ function buildProduct(row) {
 
 // --- Queue insertion ---
 
+// TK-10815 concurrency lock (Fabricut-class). The handle-exists guard is check-then-act and NOT
+// atomic: two OVERLAPPING live enqueue runs both GET-miss the handle, then both enqueue, and Quinn
+// POSTs the same handle twice → -N clones. A single-writer lockfile makes a second live enqueue fail
+// fast. Stale locks (crashed prior run) are stolen via PID-liveness.
+const LOCK = '/tmp/command54-shopify-push.lock';
+function acquireLock() {
+  for (let attempt = 0; attempt < 2; attempt++) {
+    try { const fd = fs.openSync(LOCK, 'wx'); fs.writeSync(fd, String(process.pid)); return fd; }
+    catch (e) {
+      if (e.code !== 'EEXIST') throw e;
+      let old = NaN; try { old = parseInt(fs.readFileSync(LOCK, 'utf8'), 10); } catch {}
+      let alive = false; try { if (old) { process.kill(old, 0); alive = true; } } catch { alive = false; }
+      if (alive) { console.error(`FATAL: another command54-shopify-push (pid ${old}) holds ${LOCK}. Refusing to double-fire.`); process.exit(2); }
+      try { fs.unlinkSync(LOCK); } catch {}   // stale lock — steal and retry once
+    }
+  }
+  throw new Error('could not acquire lock');
+}
+
 async function queueProducts(limit, dryRun) {
+  let lockFd;
+  if (!dryRun) {
+    lockFd = acquireLock();
+    const release = () => { try { if (lockFd !== undefined) fs.closeSync(lockFd); fs.unlinkSync(LOCK); } catch {} };
+    process.on('exit', release);
+    process.on('SIGINT', () => { release(); process.exit(130); });
+    process.on('SIGTERM', () => { release(); process.exit(143); });
+  }
   const effectiveLimit = limit || LAUNCH_PAD_BATCH;
   const limitClause = `LIMIT ${parseInt(effectiveLimit)}`;
 
@@ -474,6 +519,34 @@ async function queueProducts(limit, dryRun) {
         continue;
       }
 
+      // TK-10815 IDEMPOTENCY GUARD (Fabricut-class, queue variant): before ENQUEUEING, check whether
+      // this product's handle already exists live in Shopify. Re-running this enqueue script (or two
+      // overlapping runs) would otherwise re-queue products already live and Quinn would POST a dup →
+      // Shopify appends -1/-2… (the 2026-08-19 Fabricut double-fire class). If it's already live,
+      // reconcile the DB and skip the enqueue. Fail CLOSED — a lookup error propagates. Only runs
+      // when a token is configured (env-guarded; Quinn owns the actual POST). READ-only lookup, so
+      // no private-label leak surface.
+      if (!dryRun && SHOPIFY_TOKEN) {
+        const handle = shopifyHandle(payload.product.title);
+        const existing = await shopifyRestGet(`/products.json?handle=${encodeURIComponent(handle)}&fields=id,handle`);
+        const hit = existing && existing.products && existing.products.find(p => p.handle === handle);
+        if (hit) {
+          await pool.query(
+            `UPDATE vendor_catalog SET shopify_product_id = $1, on_shopify = true, shopify_synced_at = NOW()
+               WHERE dw_sku = $2 AND vendor_code = $3`,
+            [hit.id.toString(), row.dw_sku, VENDOR_CODE]
+          );
+          await pool.query(
+            `UPDATE enrichment_tracking SET phase8_shopify_at = NOW(), phase8_shopify_id = $1
+               WHERE vendor_code = $2 AND mfr_sku = $3`,
+            [hit.id, VENDOR_CODE, row.mfr_sku]
+          );
+          skipped++;
+          console.log(`  ↺ ${row.dw_sku} handle '${handle}' already in Shopify #${hit.id} → reconciled, not re-queued`);
+          continue;
+        }
+      }
+
       if (dryRun) {
         console.log(`  [DRY] ${row.dw_sku} | ${row.mfr_sku} (hidden)`);
         console.log(`         Title: ${payload.product.title}`);
diff --git a/DW-Programming/fabricut-daily-post.js b/DW-Programming/fabricut-daily-post.js
index 971fd3ae..938206cc 100644
--- a/DW-Programming/fabricut-daily-post.js
+++ b/DW-Programming/fabricut-daily-post.js
@@ -77,7 +77,72 @@ async function main(){
   if(!TOKEN){console.error('no SHOPIFY_ADMIN_TOKEN');process.exit(1);}
   console.log(`=== Fabricut DAILY POST ===${dry?' [DRY]':''}  min=${MIN} SKUs, FULL patterns`);
   // all eligible products, grouped by pattern, patterns ordered by their smallest dw_sku (stable progression)
-  const {rows}=await pool.query(`SELECT * FROM fabricut_catalog WHERE product_url ~ 'fabricut.com' AND on_shopify=true AND shopify_product_id IS NOT NULL AND full_scraped=true AND NULLIF(pattern_name,'') IS NOT NULL ORDER BY pattern_name, dw_sku`);
+  const {rows:rawRows}=await pool.query(`SELECT * FROM fabricut_catalog WHERE product_url ~ 'fabricut.com' AND on_shopify=true AND shopify_product_id IS NOT NULL AND full_scraped=true AND NULLIF(pattern_name,'') IS NOT NULL ORDER BY pattern_name, dw_sku`);
+  // TK-10834 recurrence guard: NEVER re-introduce an image-content-hash duplicate.
+  // Fabricut double-imported the same physical jpg under two adjacent SKUs (one row's
+  // color got the real name, the other got the trailing numeric variant code). The old
+  // UNIQUE(mfr_sku) guard was blind to it (different mfr_sku). Dedup here on
+  // (pattern_name + image content-hash), NOT on title/mfr_sku. Keeper rule: prefer the
+  // color-NAMED variant; drop the row whose color_name is a bare 3-4-digit number.
+  const imgHash = u => { const m = String(u||'').match(/\/([0-9a-f]{32})/i); return m ? m[1].toLowerCase() : ('url:'+String(u||'').trim()); };
+  const isNumericColor = c => /^\d{2,4}$/.test(String(c||'').trim());
+  // TK-10834 phase 2: the VENDOR COLORWAY IDENTITY key. The image-content-hash key above
+  // MISSED a residual class (Harlien 503 == Harlien Clay) because the vendor re-saved the
+  // catalog image under a fresh URL, so the numeric & named twins no longer shared a hash on
+  // the LIVE store. But they DO share a vendor colorway: the mfr_sku's LAST-2-DIGITS are the
+  // colorway POSITION within a pattern series (e.g. Harlien named 29097xx / numeric 29098xx,
+  // pos 03 == 03 == "Clay" == "503"). Collapsing numeric<->named twins on (pattern + colorway
+  // position) does NOT depend on the image hash matching, so a re-saved image can't hide it.
+  // We ALSO source the catalog image filename as a corroborator, matching whichever signal
+  // holds. Keeper rule (both layers): prefer the color-NAMED twin; drop the numeric one.
+  const catImgFile = u => { const m = String(u||'').match(/\/([^\/?#]+)\.(jpe?g|png|webp)/i); return m ? m[1].toLowerCase() : null; };
+  const colorwayPos = mfr => { const s = String(mfr||'').trim(); return /^\d{4,}$/.test(s) ? s.slice(-2) : null; };
+  const isNamed = r => !isNumericColor(r.color_name);
+  const dedupByHash = new Map(); // pattern|hash -> chosen row
+  const droppedDupes = [];
+  for(const r of rawRows){
+    if(!r.image_url){ dedupByHash.set('nohash:'+r.dw_sku, r); continue; } // no image -> can't be a hash-dup, keep as-is
+    const key = (r.pattern_name||'')+'|'+imgHash(r.image_url);
+    const prev = dedupByHash.get(key);
+    if(!prev){ dedupByHash.set(key, r); continue; }
+    // conflict: same pattern + same image content-hash => same colorway imported twice.
+    // keep the color-NAMED one; drop the numeric-titled twin.
+    const keepPrev = isNumericColor(r.color_name) && !isNumericColor(prev.color_name);
+    const keepNew  = isNumericColor(prev.color_name) && !isNumericColor(r.color_name);
+    if(keepNew){ droppedDupes.push({dropped:prev.dw_sku, kept:r.dw_sku, hash:imgHash(r.image_url), reason:'prev-numeric'}); dedupByHash.set(key, r); }
+    else { droppedDupes.push({dropped:r.dw_sku, kept:prev.dw_sku, hash:imgHash(r.image_url), reason: keepPrev?'new-numeric':'tie-keep-first'}); }
+  }
+  let rows = [...dedupByHash.values()];
+  if(droppedDupes.length) console.log(`[TK-10834 image-hash dedup] filtered ${droppedDupes.length} duplicate row(s) sharing a pattern+image content-hash (kept the color-named twin): `+droppedDupes.map(d=>`${d.dropped}->${d.kept}`).join(', '));
+
+  // TK-10834 phase 2 — VENDOR COLORWAY IDENTITY dedup (catches re-saved-image twins the hash layer misses).
+  // For each pattern, for each colorway position (mfr last-2), if a NUMERIC-color row and a NAMED-color row
+  // both survive, drop the numeric one — but ONLY on a confident 1:1 same-colorway match (exactly one named
+  // at that position, corroborated by a byte-identical catalog image filename when both filenames are known).
+  const byPatPos = new Map(); // pattern|pos -> {numeric:[], named:[]}
+  for(const r of rows){
+    const pos = colorwayPos(r.mfr_sku);
+    if(pos===null) continue;
+    const k = (r.pattern_name||'')+'|'+pos;
+    if(!byPatPos.has(k)) byPatPos.set(k, {numeric:[], named:[]});
+    (isNamed(r) ? byPatPos.get(k).named : byPatPos.get(k).numeric).push(r);
+  }
+  const cwDropped = new Set(); const cwLog = [];
+  for(const [k, g] of byPatPos){
+    if(g.numeric.length && g.named.length === 1){ // confident 1:1
+      const named = g.named[0];
+      for(const num of g.numeric){
+        const nf = catImgFile(num.image_url), mf = catImgFile(named.image_url);
+        const corrob = (nf && mf) ? (nf === mf) : true; // when both filenames known, require match; else accept position match
+        if(corrob){ cwDropped.add(num.dw_sku); cwLog.push(`${num.dw_sku}(${num.color_name})->${named.dw_sku}(${named.color_name})`); }
+      }
+    }
+    // >1 named at a position = ambiguous -> DO NOT drop (needs eyeball); leave both.
+  }
+  if(cwDropped.size){
+    rows = rows.filter(r => !cwDropped.has(r.dw_sku));
+    console.log(`[TK-10834 colorway-identity dedup] filtered ${cwDropped.size} numeric<->named twin(s) by vendor colorway identity (pattern+mfr colorway position, catalog-image corroborated; kept the color-named twin): `+cwLog.join(', '));
+  }
   const byPattern=new Map();
   for(const r of rows){ if(!byPattern.has(r.pattern_name)) byPattern.set(r.pattern_name,[]); byPattern.get(r.pattern_name).push(r); }
   const order=[...byPattern.keys()].sort((a,b)=>{const am=byPattern.get(a)[0].dw_sku||'',bm=byPattern.get(b)[0].dw_sku||'';return am<bm?-1:am>bm?1:0;});
diff --git a/DW-Programming/fabricut-shopify-update.js b/DW-Programming/fabricut-shopify-update.js
new file mode 100644
index 00000000..149971dc
Binary files /dev/null and b/DW-Programming/fabricut-shopify-update.js differ
diff --git a/DW-Programming/hollywood-shopify-push.js b/DW-Programming/hollywood-shopify-push.js
index 70197b04..fbd3e7f7 100644
--- a/DW-Programming/hollywood-shopify-push.js
+++ b/DW-Programming/hollywood-shopify-push.js
@@ -24,11 +24,34 @@
 
 const { Pool } = require('pg');
 const https = require('https');
+const fs = require('fs');
 
 const pool = new Pool({
   connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
 });
 
+// TK-10815 Shopify REST GET client for the pre-enqueue handle guard (env-guarded — the real POST is
+// Quinn's; this only READS to detect an already-live handle).
+const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
+const SHOPIFY_API = '2024-10';
+const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN || '').replace(/['"]/g, '').trim();
+// Shopify derives a handle from the title (lowercase, non-alphanumerics→'-', collapse/trim).
+function shopifyHandle(title) { return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
+// REST GET (fail-closed): rejects on transport/HTTP error so the caller never treats a lookup
+// failure as "handle absent". Verified Shopify 2024-10 REST honors ?handle=.
+function shopifyRestGet(path) {
+  return new Promise((resolve, reject) => {
+    const req = https.request({ host: SHOPIFY_STORE, path: `/admin/api/${SHOPIFY_API}${path}`, method: 'GET',
+      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' }, timeout: 30000 },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => {
+        if (r.statusCode >= 400) { reject(new Error(`Shopify GET ${r.statusCode}: ${d.slice(0, 200)}`)); return; }
+        try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`Shopify GET parse error: ${e.message}`)); } }); });
+    req.on('error', reject);
+    req.on('timeout', () => { req.destroy(); reject(new Error('Shopify GET timeout')); });
+    req.end();
+  });
+}
+
 const SOURCE_AGENT = 'hollywood-p8-push';
 const VENDOR_CODE = 'hollywood';
 const VENDOR_DISPLAY = 'Hollywood Wallcoverings';
@@ -285,7 +308,34 @@ function buildProduct(row) {
 
 // --- Queue insertion ---
 
+// TK-10815 concurrency lock (Fabricut-class). The handle-exists guard is check-then-act and NOT
+// atomic: two OVERLAPPING live enqueue runs both GET-miss the handle, then both enqueue, and Quinn
+// POSTs the same handle twice → -N clones. A single-writer lockfile makes a second live enqueue fail
+// fast. Stale locks (crashed prior run) are stolen via PID-liveness.
+const LOCK = '/tmp/hollywood-shopify-push.lock';
+function acquireLock() {
+  for (let attempt = 0; attempt < 2; attempt++) {
+    try { const fd = fs.openSync(LOCK, 'wx'); fs.writeSync(fd, String(process.pid)); return fd; }
+    catch (e) {
+      if (e.code !== 'EEXIST') throw e;
+      let old = NaN; try { old = parseInt(fs.readFileSync(LOCK, 'utf8'), 10); } catch {}
+      let alive = false; try { if (old) { process.kill(old, 0); alive = true; } } catch { alive = false; }
+      if (alive) { console.error(`FATAL: another hollywood-shopify-push (pid ${old}) holds ${LOCK}. Refusing to double-fire.`); process.exit(2); }
+      try { fs.unlinkSync(LOCK); } catch {}   // stale lock — steal and retry once
+    }
+  }
+  throw new Error('could not acquire lock');
+}
+
 async function queueProducts(limit, dryRun) {
+  let lockFd;
+  if (!dryRun) {
+    lockFd = acquireLock();
+    const release = () => { try { if (lockFd !== undefined) fs.closeSync(lockFd); fs.unlinkSync(LOCK); } catch {} };
+    process.on('exit', release);
+    process.on('SIGINT', () => { release(); process.exit(130); });
+    process.on('SIGTERM', () => { release(); process.exit(143); });
+  }
   const effectiveLimit = limit || LAUNCH_PAD_BATCH;
   const limitClause = `LIMIT ${parseInt(effectiveLimit)}`;
 
@@ -335,6 +385,33 @@ async function queueProducts(limit, dryRun) {
       continue;
     }
 
+    // TK-10815 IDEMPOTENCY GUARD (Fabricut-class, queue variant): before ENQUEUEING, check whether
+    // this product's handle already exists live in Shopify. Re-running this enqueue script (or two
+    // overlapping runs) would otherwise re-queue products already live and Quinn would POST a dup →
+    // Shopify appends -1/-2… (the 2026-08-19 Fabricut double-fire class). If it's already live,
+    // reconcile the DB and skip the enqueue. Fail CLOSED — a lookup error propagates. Only runs when
+    // a token is configured (env-guarded; Quinn owns the actual POST).
+    if (!dryRun && SHOPIFY_TOKEN) {
+      const handle = shopifyHandle(payload.product.title);
+      const existing = await shopifyRestGet(`/products.json?handle=${encodeURIComponent(handle)}&fields=id,handle`);
+      const hit = existing && existing.products && existing.products.find(p => p.handle === handle);
+      if (hit) {
+        await pool.query(
+          `UPDATE vendor_catalog SET shopify_product_id = $1, on_shopify = true, shopify_synced_at = NOW()
+             WHERE dw_sku = $2 AND vendor_code = $3`,
+          [hit.id.toString(), row.dw_sku, VENDOR_CODE]
+        );
+        await pool.query(
+          `UPDATE enrichment_tracking SET phase8_shopify_at = NOW(), phase8_shopify_id = $1
+             WHERE vendor_code = $2 AND mfr_sku = $3`,
+          [hit.id, VENDOR_CODE, row.mfr_sku]
+        );
+        skipped++;
+        console.log(`  ↺ ${row.mfr_sku} handle '${handle}' already in Shopify #${hit.id} → reconciled, not re-queued`);
+        continue;
+      }
+    }
+
     if (dryRun) {
       console.log(`  [DRY] ${row.dw_sku} | ${row.mfr_sku}`);
       console.log(`         Title: ${payload.product.title}`);
diff --git a/DW-Programming/justindavid-shopify-push.js b/DW-Programming/justindavid-shopify-push.js
index b3d35e7b..d3052dbb 100644
--- a/DW-Programming/justindavid-shopify-push.js
+++ b/DW-Programming/justindavid-shopify-push.js
@@ -20,6 +20,7 @@
 
 const { Pool } = require('pg');
 const https = require('https');
+const fs = require('fs');
 
 const pool = new Pool({
   connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
@@ -48,6 +49,22 @@ function shopifyGql(query, variables) {
     req.write(data); req.end();
   });
 }
+// Shopify derives a handle from the title (lowercase, non-alphanumerics→'-', collapse/trim).
+function shopifyHandle(title) { return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
+// REST GET (fail-closed): rejects on transport/HTTP error so the caller never treats a lookup
+// failure as "handle absent". Verified Shopify 2024-10 REST honors ?handle=.
+function shopifyRestGet(path) {
+  return new Promise((resolve, reject) => {
+    const req = https.request({ host: SHOPIFY_STORE, path: `/admin/api/${SHOPIFY_API}${path}`, method: 'GET',
+      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' }, timeout: 30000 },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => {
+        if (r.statusCode >= 400) { reject(new Error(`Shopify GET ${r.statusCode}: ${d.slice(0, 200)}`)); return; }
+        try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`Shopify GET parse error: ${e.message}`)); } }); });
+    req.on('error', reject);
+    req.on('timeout', () => { req.destroy(); reject(new Error('Shopify GET timeout')); });
+    req.end();
+  });
+}
 let _PUBS = null;
 async function _loadPubs() {
   if (_PUBS) return _PUBS;
@@ -306,7 +323,34 @@ function buildProduct(row) {
 
 // --- Queue insertion ---
 
+// TK-10815 concurrency lock (Fabricut-class). The handle-exists guard is check-then-act and NOT
+// atomic: two OVERLAPPING live enqueue runs both GET-miss the handle, then both enqueue, and Quinn
+// POSTs the same handle twice → -N clones. A single-writer lockfile makes a second live enqueue fail
+// fast. Stale locks (crashed prior run) are stolen via PID-liveness.
+const LOCK = '/tmp/justindavid-shopify-push.lock';
+function acquireLock() {
+  for (let attempt = 0; attempt < 2; attempt++) {
+    try { const fd = fs.openSync(LOCK, 'wx'); fs.writeSync(fd, String(process.pid)); return fd; }
+    catch (e) {
+      if (e.code !== 'EEXIST') throw e;
+      let old = NaN; try { old = parseInt(fs.readFileSync(LOCK, 'utf8'), 10); } catch {}
+      let alive = false; try { if (old) { process.kill(old, 0); alive = true; } } catch { alive = false; }
+      if (alive) { console.error(`FATAL: another justindavid-shopify-push (pid ${old}) holds ${LOCK}. Refusing to double-fire.`); process.exit(2); }
+      try { fs.unlinkSync(LOCK); } catch {}   // stale lock — steal and retry once
+    }
+  }
+  throw new Error('could not acquire lock');
+}
+
 async function queueProducts(limit, dryRun) {
+  let lockFd;
+  if (!dryRun) {
+    lockFd = acquireLock();
+    const release = () => { try { if (lockFd !== undefined) fs.closeSync(lockFd); fs.unlinkSync(LOCK); } catch {} };
+    process.on('exit', release);
+    process.on('SIGINT', () => { release(); process.exit(130); });
+    process.on('SIGTERM', () => { release(); process.exit(143); });
+  }
   const limitClause = limit ? `LIMIT ${parseInt(limit)}` : '';
 
   const { rows } = await pool.query(`
@@ -336,6 +380,32 @@ async function queueProducts(limit, dryRun) {
       continue;
     }
 
+    // TK-10815 IDEMPOTENCY GUARD (Fabricut-class, queue variant): before ENQUEUEING, check whether
+    // this product's handle already exists live in Shopify. Re-running this enqueue script (or two
+    // overlapping runs) would otherwise re-queue products already live and Quinn would POST a dup →
+    // Shopify appends -1/-2… (the 2026-08-19 Fabricut double-fire class). If it's already live,
+    // reconcile the DB and skip the enqueue. Fail CLOSED — a lookup error propagates. Only runs when
+    // a token is configured (this script's Shopify-HTTP layer is env-guarded; Quinn owns the POST).
+    if (!dryRun && SHOPIFY_TOKEN) {
+      const handle = shopifyHandle(payload.product.title);
+      const existing = await shopifyRestGet(`/products.json?handle=${encodeURIComponent(handle)}&fields=id,handle`);
+      const hit = existing && existing.products && existing.products.find(p => p.handle === handle);
+      if (hit) {
+        await pool.query(
+          `UPDATE justindavid_catalog SET shopify_product_id = $1, on_shopify = true, imported_at = NOW() WHERE dw_sku = $2`,
+          [hit.id.toString(), row.dw_sku]
+        );
+        await pool.query(
+          `UPDATE enrichment_tracking SET phase8_shopify_at = NOW(), phase8_shopify_id = $1
+             WHERE vendor_code = 'justin_david' AND mfr_sku = $2`,
+          [hit.id, row.mfr_sku]
+        );
+        skipped++;
+        console.log(`  ↺ ${row.mfr_sku} handle '${handle}' already in Shopify #${hit.id} → reconciled, not re-queued`);
+        continue;
+      }
+    }
+
     if (dryRun) {
       if (i < 10) {
         console.log(`  [DRY] ${row.dw_sku} | ${row.mfr_sku} -> ${payload.product.title}`);
diff --git a/DW-Programming/push-maya-romanoff-to-shopify.js b/DW-Programming/push-maya-romanoff-to-shopify.js
index f918f18f..098cdd52 100644
--- a/DW-Programming/push-maya-romanoff-to-shopify.js
+++ b/DW-Programming/push-maya-romanoff-to-shopify.js
@@ -161,6 +161,26 @@ function buildBodyHtml(product, creativeColor) {
   return `${specsTable}${description}`;
 }
 
+// ── Shopify derives a handle from the title (lowercase, non-alphanumerics→'-', collapse/trim). ──
+function shopifyHandle(title) { return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
+
+// ── Shopify REST GET (fail-closed — throws propagate to caller; never swallow into "not found"). ──
+async function shopifyGet(endpoint) {
+  const url = `https://${CONFIG.SHOPIFY_STORE}/admin/api/${CONFIG.API_VERSION}${endpoint}`;
+  const res = await fetch(url, {
+    method: 'GET',
+    headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': CONFIG.SHOPIFY_TOKEN }
+  });
+  if (res.status === 429) {
+    const retryAfter = parseInt(res.headers.get('Retry-After') || '2', 10);
+    console.log(`  ⏳ Rate limited (GET) — waiting ${retryAfter}s...`);
+    await sleep(retryAfter * 1000);
+    return shopifyGet(endpoint);
+  }
+  if (!res.ok) throw new Error(`Shopify GET ${res.status}: ${(await res.text()).slice(0, 200)}`);
+  return res.json();
+}
+
 // ── Shopify REST API Call ──
 async function shopifyPost(endpoint, body) {
   const url = `https://${CONFIG.SHOPIFY_STORE}/admin/api/${CONFIG.API_VERSION}${endpoint}`;
@@ -223,9 +243,33 @@ function buildProductImages(product) {
   return images;
 }
 
+// TK-10815 concurrency lock (Fabricut-class). The handle-exists guard is check-then-act and NOT
+// atomic: two OVERLAPPING runs both GET-miss the handle, then both POST, re-creating the same -N
+// clones. A single-writer lockfile makes a second run fail fast. Stale locks (crashed prior run) are
+// stolen via PID-liveness. This script has no dry-run mode — every run writes, so always lock.
+const LOCK = '/tmp/push-maya-romanoff-to-shopify.lock';
+function acquireLock() {
+  for (let attempt = 0; attempt < 2; attempt++) {
+    try { const fd = fs.openSync(LOCK, 'wx'); fs.writeSync(fd, String(process.pid)); return fd; }
+    catch (e) {
+      if (e.code !== 'EEXIST') throw e;
+      let old = NaN; try { old = parseInt(fs.readFileSync(LOCK, 'utf8'), 10); } catch {}
+      let alive = false; try { if (old) { process.kill(old, 0); alive = true; } } catch { alive = false; }
+      if (alive) { console.error(`FATAL: another push-maya-romanoff-to-shopify (pid ${old}) holds ${LOCK}. Refusing to double-fire.`); process.exit(2); }
+      try { fs.unlinkSync(LOCK); } catch {}   // stale lock — steal and retry once
+    }
+  }
+  throw new Error('could not acquire lock');
+}
+
 // ── Main Push Function ──
 async function main() {
   const startTime = Date.now();
+  const lockFd = acquireLock();
+  const release = () => { try { if (lockFd !== undefined) fs.closeSync(lockFd); fs.unlinkSync(LOCK); } catch {} };
+  process.on('exit', release);
+  process.on('SIGINT', () => { release(); process.exit(130); });
+  process.on('SIGTERM', () => { release(); process.exit(143); });
   console.log('🚀 Maya Romanoff — Push New Products to Shopify');
   console.log('================================================\n');
 
@@ -260,6 +304,7 @@ async function main() {
 
   let pushed = 0;
   let errors = 0;
+  let skipped = 0;
   const errorList = [];
 
   for (let i = 0; i < products.length; i++) {
@@ -285,6 +330,30 @@ async function main() {
 
     const title = `${titleParts.join(' ')} Wallcovering | Maya Romanoff`;
 
+    // TK-10815 IDEMPOTENCY GUARD (Fabricut-class): never re-create a product whose handle already
+    // exists in Shopify. The shopify_product_id IS NULL query is not enough — a stale flag, a partial
+    // prior run, or two overlapping runs POST the same handle twice and Shopify appends -1/-2… (the
+    // 2026-08-19 Fabricut double-fire that created 90 clone DRAFTs). Fail CLOSED — a lookup error
+    // propagates (never fall through to POST). If the handle is live, reconcile the DB and skip.
+    const handle = shopifyHandle(title);
+    const existing = await shopifyGet(`/products.json?handle=${encodeURIComponent(handle)}&fields=id,handle`);
+    const hit = existing && existing.products && existing.products.find(x => x.handle === handle);
+    if (hit) {
+      const updateClient = await pool.connect();
+      try {
+        await updateClient.query(
+          'UPDATE maya_catalog SET shopify_product_id = $1, imported_at = NOW() WHERE mfr_sku = $2',
+          [hit.id.toString(), p.mfr_sku]
+        );
+      } finally {
+        updateClient.release();
+      }
+      skipped++;
+      console.log(`  ↺ [${i + 1}/${products.length}] handle '${handle}' already in Shopify #${hit.id} → reconciled, no dup created`);
+      if (i < products.length - 1) await sleep(CONFIG.DELAY_MS);
+      continue;
+    }
+
     // SEO title
     const seoTitle = creativeColor
       ? `Shop ${toTitleCase(p.pattern_name || p.mfr_sku)} in ${toTitleCase(creativeColor)} | Maya Romanoff`
@@ -412,6 +481,7 @@ async function main() {
   console.log('\n================================================');
   console.log(`🏁 COMPLETE — ${duration} min`);
   console.log(`   ✅ Pushed: ${pushed}`);
+  console.log(`   ↺ Skipped (handle exists): ${skipped}`);
   console.log(`   ❌ Errors: ${errors}`);
   if (errorList.length > 0) {
     console.log('\n   Failed products:');
diff --git a/mailers/assets/pj-fall-2026/probe.html b/mailers/assets/pj-fall-2026/probe.html
new file mode 100644
index 00000000..8a6b74cc
--- /dev/null
+++ b/mailers/assets/pj-fall-2026/probe.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>404 - Page Not Found | Phillip Jeffries</title>
+  <script src="https://cdn.tailwindcss.com"></script>
+  <style>
+    /* Custom styles for hover effects */
+    .nav-link {
+      position: relative;
+      overflow: hidden;
+    }
+    
+    .nav-link-bg {
+      position: absolute;
+      inset: 0;
+      background-color: transparent;
+      border: 1px solid rgba(255, 255, 255, 0.3);
+      transition: all 0.3s;
+    }
+    
+    .nav-link:hover .nav-link-bg {
+      background-color: white;
+      border-color: white;
+    }
+    
+    .nav-link-text {
+      position: relative;
+      color: white;
+      letter-spacing: 0.05em;
+      transition: color 0.3s;
+    }
+    
+    .nav-link:hover .nav-link-text {
+      color: black;
+    }
+    
+    .nav-link-icon {
+      position: relative;
+      color: white;
+      transition: color 0.3s;
+    }
+    
+    .nav-link:hover .nav-link-icon {
+      color: black;
+    }
+  </style>
+</head>
+<body>
+  <div class="relative min-h-screen w-full overflow-hidden">
+    <!-- Background Image with Overlay -->
+    <div class="absolute inset-0 z-0">
+      <img
+        src="https://cms.phillipjeffries.com/files/general/error_page_background.jpg"
+        alt="Luxury wallpaper background"
+        class="w-full h-full object-cover"
+      />
+      <div class="absolute inset-0 bg-black/70"></div>
+    </div>
+
+    <!-- Content -->
+    <div class="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 py-12">
+      <div class="max-w-3xl w-full text-center space-y-12">
+        <!-- 404 Message -->
+        <div class="space-y-8">
+          <div class="space-y-3">
+            <h2 class="text-white text-7xl md:text-8xl tracking-tight">404</h2>
+            <div class="h-px w-24 bg-white/40 mx-auto"></div>
+            <p class="text-white/95 text-2xl md:text-3xl tracking-wide">Page Not Found</p>
+          </div>
+          <p class="text-white/80 text-lg md:text-xl max-w-xl mx-auto leading-relaxed">
+            You've reached a blank wall.<br>
+            Not our usual look. Let's find something more your style.
+          </p>
+        </div>
+
+        <!-- Navigation Links -->
+        <div class="space-y-6">
+          <p class="text-white/60 tracking-widest uppercase text-sm">Explore Our Collections</p>
+          <div class="flex flex-col sm:flex-row gap-3 justify-center items-center">
+            <a
+              href="https://www.phillipjeffries.com/shop/wallcoverings"
+              class="nav-link px-8 py-3 w-full sm:w-56"
+            >
+              <div class="nav-link-bg"></div>
+              <span class="nav-link-text whitespace-nowrap">Shop All</span>
+            </a>
+            <a
+              href="https://www.phillipjeffries.com/shop/whats-new"
+              class="nav-link px-8 py-3 w-full sm:w-56"
+            >
+              <div class="nav-link-bg"></div>
+              <span class="nav-link-text whitespace-nowrap">What's New</span>
+            </a>
+            <a
+              href="https://www.phillipjeffries.com/shop/collections"
+              class="nav-link px-8 py-3 w-full sm:w-56"
+            >
+              <div class="nav-link-bg"></div>
+              <span class="nav-link-text whitespace-nowrap">Shop by Category</span>
+            </a>
+          </div>
+        </div>
+
+        <!-- Home Button -->
+        <div class="pt-4">
+          <a 
+            href="https://www.phillipjeffries.com/"
+            class="nav-link inline-flex items-center justify-center px-8 py-3 w-full sm:w-56"
+          >
+            <div class="nav-link-bg"></div>
+            <svg class="nav-link-icon mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
+              <polyline points="9 22 9 12 15 12 15 22"/>
+            </svg>
+            <span class="nav-link-text whitespace-nowrap">Return to Home</span>
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</body>
+</html>
diff --git a/shopify/scripts/maharam-gmc-unpublish.js b/shopify/scripts/maharam-gmc-unpublish.js
new file mode 100644
index 00000000..40bf57ca
--- /dev/null
+++ b/shopify/scripts/maharam-gmc-unpublish.js
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+/**
+ * MaharamSOURCE — remove all DWAX- (Maharam) products from the Google & YouTube
+ * sales channel so the $4.25-sample-as-price GMC disapproval vector stops.
+ *
+ * Reversible: the exact working-set of product IDs is written to a manifest, and a
+ * matching re-publish (rollback) script is emitted, so `--apply` is one-command undo-able.
+ *
+ * DRY-RUN by default (read-only: enumerates + writes manifest, makes NO mutation).
+ *   node scripts/maharam-gmc-unpublish.js
+ * LIVE (customer-facing — GATED, Steve-approved only):
+ *   node scripts/maharam-gmc-unpublish.js --apply --yes-i-am-steve
+ */
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const GOOGLE_PUB = 'gid://shopify/Publication/29646651457'; // Google & YouTube
+const APPLY = process.argv.includes('--apply');
+const CONFIRM = process.argv.includes('--yes-i-am-steve');
+const OUTDIR = path.join(__dirname, 'maharam-gmc-unpublish.out');
+
+function token() {
+  for (const p of [path.join(process.env.HOME, 'Projects/secrets-manager/.env'),
+                    path.join(__dirname, '../.env'), path.join(__dirname, '../../.env')]) {
+    try {
+      const m = fs.readFileSync(p, 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
+      if (m) return m[1].replace(/["']/g, '').trim();
+    } catch {}
+  }
+  throw new Error('SHOPIFY_ADMIN_TOKEN not found');
+}
+const TOKEN = token();
+
+async function gql(query, variables) {
+  const r = 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 }),
+  });
+  const j = await r.json();
+  if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors).slice(0, 300));
+  // throttle courtesy
+  const cost = j.extensions && j.extensions.cost && j.extensions.cost.throttleStatus;
+  if (cost && cost.currentlyAvailable < 200) await new Promise(z => setTimeout(z, 700));
+  return j.data;
+}
+
+async function collectWorkingSet() {
+  const q = `query($cur:String,$g:ID!){ products(first:100, query:"sku:DWAX-*", after:$cur){
+    pageInfo{hasNextPage endCursor}
+    edges{ node{ id title status onGoogle:publishedOnPublication(publicationId:$g) } } } }`;
+  let cur = null, ids = [];
+  do {
+    const d = await gql(q, { cur, g: GOOGLE_PUB });
+    for (const e of d.products.edges) {
+      if (e.node.onGoogle) ids.push({ id: e.node.id, title: e.node.title, status: e.node.status });
+    }
+    cur = d.products.pageInfo.hasNextPage ? d.products.pageInfo.endCursor : null;
+  } while (cur);
+  return ids;
+}
+
+const M_UNPUB = `mutation($id:ID!,$input:[PublicationInput!]!){
+  publishableUnpublish(id:$id, input:$input){
+    publishable{ availablePublicationsCount{ count } } userErrors{ field message } } }`;
+
+(async () => {
+  fs.mkdirSync(OUTDIR, { recursive: true });
+  console.log(`[MaharamSOURCE] Google&YouTube unpublish — mode: ${APPLY ? 'APPLY (LIVE)' : 'DRY-RUN'}`);
+  const set = await collectWorkingSet();
+  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+  const manifest = path.join(OUTDIR, `working-set-${stamp}.json`);
+  fs.writeFileSync(manifest, JSON.stringify({ publication: GOOGLE_PUB, count: set.length, products: set }, null, 2));
+  // rollback script (re-publish the exact set)
+  const rollback = path.join(OUTDIR, `ROLLBACK-republish-${stamp}.js`);
+  fs.writeFileSync(rollback, `#!/usr/bin/env node
+// AUTO-GENERATED rollback for MaharamSOURCE unpublish ${stamp}.
+// Re-publishes the exact ${set.length} products back onto Google & YouTube (${GOOGLE_PUB}).
+const fs=require('fs'),path=require('path');
+const STORE='${STORE}',API='${API}',PUB='${GOOGLE_PUB}';
+const set=require(${JSON.stringify(manifest)}).products;
+const TOKEN=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].replace(/["']/g,'').trim();
+const M='mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{message}}}';
+(async()=>{let ok=0,err=0;for(const p of set){const r=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:M,variables:{id:p.id,input:[{publicationId:PUB}]}})});const j=await r.json();const e=j.data&&j.data.publishablePublish.userErrors||[];if(e.length){err++;console.error(p.id,e)}else ok++;await new Promise(z=>setTimeout(z,120));}console.log('republished',ok,'errors',err);})();
+`);
+
+  console.log(`  working-set on Google channel : ${set.length}`);
+  console.log(`  status breakdown              :`, set.reduce((a,p)=>{a[p.status]=(a[p.status]||0)+1;return a;},{}));
+  console.log(`  manifest (reversibility)      : ${manifest}`);
+  console.log(`  rollback script (re-publish)  : ${rollback}`);
+  console.log(`  mutation to run per product   : publishableUnpublish(id, input:[{publicationId:"${GOOGLE_PUB}"}])`);
+
+  if (!APPLY) {
+    console.log(`\n  DRY-RUN — no mutations sent. To apply (Steve-gated):`);
+    console.log(`    node scripts/maharam-gmc-unpublish.js --apply --yes-i-am-steve`);
+    return;
+  }
+  if (!CONFIRM) { console.error('\n  REFUSED: --apply requires --yes-i-am-steve (customer-facing gate).'); process.exit(2); }
+
+  let ok = 0, err = 0;
+  for (const p of set) {
+    try {
+      const d = await gql(M_UNPUB, { id: p.id, input: [{ publicationId: GOOGLE_PUB }] });
+      const ue = d.publishableUnpublish.userErrors || [];
+      if (ue.length) { err++; console.error('  ERR', p.id, JSON.stringify(ue)); }
+      else { ok++; if (ok % 100 === 0) console.log(`  …unpublished ${ok}/${set.length}`); }
+    } catch (e) { err++; console.error('  EXC', p.id, e.message); }
+    await new Promise(z => setTimeout(z, 120));
+  }
+  console.log(`\n  DONE — unpublished ${ok}, errors ${err}. Rollback: node ${path.relative(process.cwd(), rollback)}`);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-28-08-055Z.js b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-28-08-055Z.js
new file mode 100644
index 00000000..2a3dc427
--- /dev/null
+++ b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-28-08-055Z.js
@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+// AUTO-GENERATED rollback for MaharamSOURCE unpublish 2026-08-26T19-28-08-055Z.
+// Re-publishes the exact 1429 products back onto Google & YouTube (gid://shopify/Publication/29646651457).
+const fs=require('fs'),path=require('path');
+const STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10',PUB='gid://shopify/Publication/29646651457';
+const set=require("/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/scripts/maharam-gmc-unpublish.out/working-set-2026-08-26T19-28-08-055Z.json").products;
+const TOKEN=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].replace(/["']/g,'').trim();
+const M='mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{message}}}';
+(async()=>{let ok=0,err=0;for(const p of set){const r=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:M,variables:{id:p.id,input:[{publicationId:PUB}]}})});const j=await r.json();const e=j.data&&j.data.publishablePublish.userErrors||[];if(e.length){err++;console.error(p.id,e)}else ok++;await new Promise(z=>setTimeout(z,120));}console.log('republished',ok,'errors',err);})();
diff --git a/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-36-35-341Z.js b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-36-35-341Z.js
new file mode 100644
index 00000000..1c11b4d0
--- /dev/null
+++ b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-36-35-341Z.js
@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+// AUTO-GENERATED rollback for MaharamSOURCE unpublish 2026-08-26T19-36-35-341Z.
+// Re-publishes the exact 1429 products back onto Google & YouTube (gid://shopify/Publication/29646651457).
+const fs=require('fs'),path=require('path');
+const STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10',PUB='gid://shopify/Publication/29646651457';
+const set=require("/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/scripts/maharam-gmc-unpublish.out/working-set-2026-08-26T19-36-35-341Z.json").products;
+const TOKEN=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].replace(/["']/g,'').trim();
+const M='mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{message}}}';
+(async()=>{let ok=0,err=0;for(const p of set){const r=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:M,variables:{id:p.id,input:[{publicationId:PUB}]}})});const j=await r.json();const e=j.data&&j.data.publishablePublish.userErrors||[];if(e.length){err++;console.error(p.id,e)}else ok++;await new Promise(z=>setTimeout(z,120));}console.log('republished',ok,'errors',err);})();
diff --git a/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-45-53-920Z.js b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-45-53-920Z.js
new file mode 100644
index 00000000..f7c8ead9
--- /dev/null
+++ b/shopify/scripts/maharam-gmc-unpublish.out/ROLLBACK-republish-2026-08-26T19-45-53-920Z.js
@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+// AUTO-GENERATED rollback for MaharamSOURCE unpublish 2026-08-26T19-45-53-920Z.
+// Re-publishes the exact 645 products back onto Google & YouTube (gid://shopify/Publication/29646651457).
+const fs=require('fs'),path=require('path');
+const STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10',PUB='gid://shopify/Publication/29646651457';
+const set=require("/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/scripts/maharam-gmc-unpublish.out/working-set-2026-08-26T19-45-53-920Z.json").products;
+const TOKEN=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].replace(/["']/g,'').trim();
+const M='mutation($id:ID!,$input:[PublicationInput!]!){publishablePublish(id:$id,input:$input){userErrors{message}}}';
+(async()=>{let ok=0,err=0;for(const p of set){const r=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:M,variables:{id:p.id,input:[{publicationId:PUB}]}})});const j=await r.json();const e=j.data&&j.data.publishablePublish.userErrors||[];if(e.length){err++;console.error(p.id,e)}else ok++;await new Promise(z=>setTimeout(z,120));}console.log('republished',ok,'errors',err);})();
diff --git a/shopify/scripts/maharam-price-load.js b/shopify/scripts/maharam-price-load.js
new file mode 100644
index 00000000..f23b97f2
--- /dev/null
+++ b/shopify/scripts/maharam-price-load.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+/**
+ * MaharamSOURCE — load real per-yard prices onto live Maharam (DWAX-) products.
+ * Mirrors the DW yard-sold convention (ref: Kravet DWKK-103268):
+ *   option "Size" = [ "Sold Per Yard - <W>In Wide" (pos1, sellable, sku=DW_SKU, price=$/yd),
+ *                     "Sample" (pos2, sku=DW_SKU-Sample, $4.25) ]
+ *   + metafields: global.unit_of_measure="Priced Per Yard", dwc.order_unit="Yard",
+ *     global.v_prods_quantity_order_units="1", global.v_prods_quantity_order_min="1"
+ *
+ * SAFE + surgical: renames the existing "Type" option to "Size", ADDS the sellable variant
+ * (never deletes the Sample), reorders sellable to position 1, sets metafields.
+ * Idempotent: skips a product that already has a non-Sample variant.
+ *
+ *   node scripts/maharam-price-load.js --pilot                 # dry-run, 1 SKU
+ *   node scripts/maharam-price-load.js --pilot --apply --yes-i-am-steve
+ *   node scripts/maharam-price-load.js --apply --yes-i-am-steve            # all 1,429
+ */
+'use strict';
+const fs = require('fs'), path = require('path');
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const APPLY = process.argv.includes('--apply');
+const CONFIRM = process.argv.includes('--yes-i-am-steve');
+const PILOT = process.argv.includes('--pilot');
+const PILOT_SKU = 'DWAX-1025005'; // Accord argentite, $79/yd, 36"
+const TOKEN = (() => {
+  for (const p of [path.join(process.env.HOME,'Projects/secrets-manager/.env'), path.join(__dirname,'../.env')]) {
+    try { const m = fs.readFileSync(p,'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m); if (m) return m[1].replace(/["']/g,'').trim(); } catch {}
+  } throw new Error('no token');
+})();
+const OUT = path.join(__dirname, 'maharam-price-load.out'); fs.mkdirSync(OUT, {recursive:true});
+
+async function gql(query, variables) {
+  const r = 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}) });
+  const j = await r.json();
+  if (j.errors) throw new Error('GQL '+JSON.stringify(j.errors).slice(0,300));
+  const t = j.extensions?.cost?.throttleStatus; if (t && t.currentlyAvailable<300) await new Promise(z=>setTimeout(z,600));
+  return j.data;
+}
+const widthIn = (w) => { const m = String(w||'').match(/(\d+(?:\.\d+)?)\s*"/); return m?m[1]:null; };
+
+const Q_PROD = `query($q:String!){ products(first:1, query:$q){ edges{ node{
+  id title status
+  options{ id name optionValues{ id name } }
+  variants(first:20){ edges{ node{ id title sku price position selectedOptions{name value} } } } } } } }`;
+
+const M_OPT_RENAME = `mutation($pid:ID!,$oid:ID!){ productOptionUpdate(productId:$pid, option:{id:$oid, name:"Size"}){ userErrors{field message} } }`;
+const M_VAR_CREATE = `mutation($pid:ID!,$vars:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$pid, variants:$vars, strategy: DEFAULT){
+    productVariants{ id title price sku } userErrors{ field message } } }`;
+const M_VAR_REORDER = `mutation($pid:ID!,$pos:[ProductVariantPositionInput!]!){
+  productVariantsBulkReorder(productId:$pid, positions:$pos){ userErrors{ field message } } }`;
+const M_MF_SET = `mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{ field message } } }`;
+
+async function loadOne(rec, dry) {
+  const q = `sku:${rec.mfr_sku ? '' : ''}${rec.dw_sku}`; // products carry dw_sku as the sellable sku target; query by dw_sku sample
+  const data = await gql(Q_PROD, {q:`sku:${rec.dw_sku}-Sample`});
+  const node = data.products.edges[0]?.node;
+  if (!node) return {sku:rec.dw_sku, status:'NOT_FOUND'};
+  const hasSellable = node.variants.edges.some(e => !/sample/i.test(e.node.title));
+  if (hasSellable) return {sku:rec.dw_sku, status:'ALREADY_HAS_SELLABLE'};
+  const w = widthIn(rec.width); const yTitle = `Sold Per Yard - ${w||'54'}In Wide`;
+  const opt = node.options[0];
+  const plan = {
+    sku: rec.dw_sku, title: node.title, price: rec.price_per_yard, width: w,
+    yardVariantTitle: yTitle, renameOptionFrom: opt?.name,
+  };
+  if (dry) return {...plan, status:'DRY'};
+  // 1. rename option -> Size
+  if (opt && opt.name !== 'Size') { const r=await gql(M_OPT_RENAME,{pid:node.id,oid:opt.id}); if(r.productOptionUpdate.userErrors.length) return {...plan,status:'ERR_OPT',err:r.productOptionUpdate.userErrors}; }
+  // 2. add sellable yard variant
+  const cr = await gql(M_VAR_CREATE, {pid:node.id, vars:[{
+    optionValues:[{optionName:'Size', name:yTitle}],
+    price:String(rec.price_per_yard),
+    inventoryItem:{sku:rec.dw_sku, tracked:false},
+    inventoryPolicy:'CONTINUE'
+  }]});
+  if (cr.productVariantsBulkCreate.userErrors.length) return {...plan,status:'ERR_VAR',err:cr.productVariantsBulkCreate.userErrors};
+  const newVarId = cr.productVariantsBulkCreate.productVariants[0].id;
+  const sampleVar = node.variants.edges[0].node.id;
+  // 3. reorder: sellable pos1, sample pos2
+  await gql(M_VAR_REORDER, {pid:node.id, pos:[{id:newVarId,position:1},{id:sampleVar,position:2}]});
+  // 4. metafields
+  await gql(M_MF_SET, {mf:[
+    {ownerId:node.id, namespace:'global', key:'unit_of_measure', type:'single_line_text_field', value:'Priced Per Yard'},
+    {ownerId:node.id, namespace:'dwc', key:'order_unit', type:'single_line_text_field', value:'Yard'},
+    {ownerId:node.id, namespace:'global', key:'v_prods_quantity_order_units', type:'single_line_text_field', value:'1'},
+    {ownerId:node.id, namespace:'global', key:'v_prods_quantity_order_min', type:'single_line_text_field', value:'1'},
+  ]});
+  return {...plan, status:'LOADED', newVarId};
+}
+
+(async () => {
+  let recs = JSON.parse(fs.readFileSync(path.join(__dirname,'../.playwright-mcp/maharam-sku-prices.json'),'utf8'))
+             .filter(r => r.price_per_yard);
+  if (PILOT) recs = recs.filter(r => r.dw_sku === PILOT_SKU);
+  console.log(`[MaharamSOURCE price-load] mode=${APPLY?'APPLY':'DRY-RUN'} ${PILOT?'(PILOT '+PILOT_SKU+')':''} · ${recs.length} product(s)`);
+  if (APPLY && !CONFIRM) { console.error('REFUSED: --apply needs --yes-i-am-steve (customer-facing).'); process.exit(2); }
+  const results = []; let n=0;
+  for (const rec of recs) {
+    try { const r = await loadOne(rec, !APPLY); results.push(r);
+      if (APPLY) { n++; if (n%50===0) console.log(`  …${n}/${recs.length}`); }
+    } catch(e){ results.push({sku:rec.dw_sku, status:'EXC', err:e.message}); }
+    if (APPLY) await new Promise(z=>setTimeout(z,120));
+  }
+  const stamp=new Date().toISOString().replace(/[:.]/g,'-');
+  fs.writeFileSync(path.join(OUT,`run-${stamp}.json`), JSON.stringify(results,null,1));
+  const by = results.reduce((a,r)=>{a[r.status]=(a[r.status]||0)+1;return a;},{});
+  console.log('  summary:', by);
+  if (!APPLY) results.slice(0,3).forEach(r=>console.log('  DRY sample:', JSON.stringify(r)));
+  console.log('  results ->', path.join(OUT,`run-${stamp}.json`));
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/shopify/scripts/zoffany-work/zof-recheck.mjs b/shopify/scripts/zoffany-work/zof-recheck.mjs
new file mode 100644
index 00000000..a8ef99f4
--- /dev/null
+++ b/shopify/scripts/zoffany-work/zof-recheck.mjs
@@ -0,0 +1,7 @@
+const TOKEN=process.env.TOKEN,STORE="designer-laboratory-sandbox.myshopify.com",API="2024-10";
+const url=`https://${STORE}/admin/api/${API}/graphql.json`;
+const gql=async(q,v)=>{const r=await fetch(url,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});return (await r.json()).data;};
+const Q=`query($c:String){products(first:100,query:"vendor:Zoffany status:active",after:$c){pageInfo{hasNextPage endCursor}edges{node{legacyResourceId variants(first:20){edges{node{title sku}}}}}}}`;
+let c=null,total=0,missing=0,badsku=0;
+do{const d=await gql(Q,{c});const p=d.products;for(const e of p.edges){total++;const vs=e.node.variants.edges.map(x=>x.node);const hs=vs.some(v=>/sample/i.test(v.title||''));if(!hs)missing++;const s=vs.find(v=>/sample/i.test(v.title||''));if(s&&!/-sample$/i.test(s.sku||''))badsku++;}c=p.pageInfo.hasNextPage?p.pageInfo.endCursor:null;}while(c);
+console.log('ACTIVE',total,'| missing_sample',missing,'| sample_with_bad_sku',badsku);
diff --git a/shopify/scripts/zoffany-work/zof-repair.mjs b/shopify/scripts/zoffany-work/zof-repair.mjs
new file mode 100644
index 00000000..e25aa348
--- /dev/null
+++ b/shopify/scripts/zoffany-work/zof-repair.mjs
@@ -0,0 +1,22 @@
+import fs from 'fs';
+const TOKEN=process.env.TOKEN, STORE="designer-laboratory-sandbox.myshopify.com", API="2024-10";
+const url=`https://${STORE}/admin/api/${API}/graphql.json`;
+const gql=async(q,v)=>{const r=await fetch(url,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});return (await r.json());};
+const rmap=JSON.parse(fs.readFileSync('zoffany-restore-map.json')).products;
+const ids=["7793952849971","7793952882739"];
+// guard: only repair if the product currently has ONLY a Sample variant (i.e. still broken)
+const QCHECK=`query($id:ID!){product(id:$id){variants(first:10){edges{node{title sku price}}}}}`;
+const M=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkCreate(productId:$pid,variants:$variants){productVariants{id title sku price} userErrors{field message}}}`;
+for(const id of ids){
+  const gid=`gid://shopify/Product/${id}`;
+  const cur=(await gql(QCHECK,{id:gid})).data?.product?.variants?.edges?.map(e=>e.node)||[];
+  const hasRoll=cur.some(v=>!/sample/i.test(v.title||''));
+  if(hasRoll){console.log(id,'ALREADY OK (has non-sample variant), skipping:',cur.map(v=>v.title+':'+v.sku).join(','));continue;}
+  const orig=rmap.find(m=>String(m.id)===id).variants[0];
+  const vin=[{price:String(orig.price), optionValues:[{optionName:"Title", name:"Sold Per Roll"}], inventoryItem:{sku:orig.sku, tracked:false}, inventoryPolicy:"CONTINUE"}];
+  const d=await gql(M,{pid:gid,variants:vin});
+  console.log(id, JSON.stringify(d.data?.productVariantsBulkCreate));
+}
+// final verify
+const QV=`query($id:ID!){product(id:$id){title variants(first:10){edges{node{title sku price}}}}}`;
+for(const id of ids){const d=(await gql(QV,{id:`gid://shopify/Product/${id}`})).data;console.log('VERIFY',id,d.product.title,'=>',d.product.variants.edges.map(e=>`${e.node.title}:${e.node.sku}:$${e.node.price}`).join('  |  '));}
diff --git a/shopify/scripts/zoffany-work/zof-reprice.mjs b/shopify/scripts/zoffany-work/zof-reprice.mjs
new file mode 100644
index 00000000..75f56abd
--- /dev/null
+++ b/shopify/scripts/zoffany-work/zof-reprice.mjs
@@ -0,0 +1,56 @@
+import fs from 'fs';
+// Zoffany below-cost reprice → DW-standard "vendor cost plus": retail = cost/0.65/0.85
+// Steve-authorized basis 2026-08-26: zoffany_catalog.price_trade = vendor NET cost.
+// Mirrors the proven zof-repair.mjs mechanic (same store/API/gql). Restore-map written BEFORE each write.
+// Usage: TOKEN=$SHOPIFY_ADMIN_TOKEN node zof-reprice.mjs <verify|apply>
+const TOKEN = process.env.TOKEN, STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+if (!TOKEN) { console.error('NO TOKEN (set TOKEN=$SHOPIFY_ADMIN_TOKEN)'); process.exit(1); }
+const url = `https://${STORE}/admin/api/${API}/graphql.json`;
+const gql = async (q, v) => {
+  const r = await fetch(url, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
+  return await r.json();
+};
+const MODE = process.argv[2] || 'verify';
+const SANITY_MAX_UPLIFT = 4.0; // below-cost fix is ~2.54x; anything >4x = bad cost data → SKIP, never write
+
+const reprice = JSON.parse(fs.readFileSync('/tmp/zoffany-reprice-79.json', 'utf8'));
+const active = JSON.parse(fs.readFileSync('zoffany-active-all.json', 'utf8'));
+
+// dw_sku -> live Shopify product id (match the SELLABLE, non-sample variant)
+const skuToProduct = {};
+for (const p of active) for (const v of (p.variants || [])) {
+  if (v.sku && !/-sample$/i.test(v.sku)) skuToProduct[v.sku] = p.id;
+}
+
+const restorePath = 'zoffany-reprice-restore-map.json';
+const restore = fs.existsSync(restorePath) ? JSON.parse(fs.readFileSync(restorePath, 'utf8')) : {};
+const Q = `query($id:ID!){product(id:$id){id title variants(first:20){edges{node{id title sku price}}}}}`;
+const M = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$variants){productVariants{id sku price} userErrors{field message}}}`;
+
+let done = 0, skip = 0, fail = 0, suspect = 0;
+const rows = [];
+for (const r of reprice) {
+  const pid = skuToProduct[r.dw_sku];
+  if (!pid) { skip++; rows.push({ dw_sku: r.dw_sku, status: 'NO_PRODUCT_MATCH' }); continue; }
+  const prod = (await gql(Q, { id: `gid://shopify/Product/${pid}` })).data?.product;
+  if (!prod) { fail++; rows.push({ dw_sku: r.dw_sku, status: 'GET_FAIL' }); continue; }
+  const sell = prod.variants.edges.map(e => e.node).find(n => !/sample/i.test(n.title || '') && !/-sample$/i.test(n.sku || ''));
+  if (!sell) { skip++; rows.push({ dw_sku: r.dw_sku, status: 'NO_SELLABLE_VARIANT' }); continue; }
+  const oldP = Number(sell.price), newP = Number(r.new_price);
+  const uplift = oldP > 0 ? +(newP / oldP).toFixed(2) : null;
+  const rec = { dw_sku: r.dw_sku, title: prod.title, old: sell.price, new: r.new_price, uplift, variant: sell.id };
+  rows.push(rec);
+  if (uplift && uplift > SANITY_MAX_UPLIFT) { suspect++; rec.status = 'SUSPECT_SKIP_uplift>' + SANITY_MAX_UPLIFT; continue; }
+  if (MODE === 'apply') {
+    restore[sell.id] = { product: pid, dw_sku: r.dw_sku, old_price: sell.price };
+    fs.writeFileSync(restorePath, JSON.stringify(restore, null, 2)); // persist BEFORE write
+    const d = await gql(M, { pid: `gid://shopify/Product/${pid}`, variants: [{ id: sell.id, price: String(r.new_price) }] });
+    const err = d.data?.productVariantsBulkUpdate?.userErrors || [];
+    if (err.length) { fail++; rec.status = 'ERR'; rec.err = err; } else { done++; rec.status = 'REPRICED'; }
+    await new Promise(res => setTimeout(res, 400));
+  } else { rec.status = 'WOULD_REPRICE'; }
+}
+fs.writeFileSync(`zof-reprice-${MODE}.json`, JSON.stringify({ mode: MODE, priced: done, skipped: skip, suspect, failed: fail, rows }, null, 2));
+console.log(`MODE=${MODE}  priced=${done}  would/skip=${skip}  suspect=${suspect}  fail=${fail}  (full: zof-reprice-${MODE}.json)`);
+for (const x of rows.slice(0, 8)) console.log(` ${x.dw_sku}  ${x.old} -> ${x.new}  (${x.uplift}x)  ${x.status}`);
+// Undo after apply: for each id in zoffany-reprice-restore-map.json, productVariantsBulkUpdate price back to old_price.
diff --git a/shopify/scripts/zoffany-work/zof-sample-run.mjs b/shopify/scripts/zoffany-work/zof-sample-run.mjs
new file mode 100644
index 00000000..21d3886c
--- /dev/null
+++ b/shopify/scripts/zoffany-work/zof-sample-run.mjs
@@ -0,0 +1,44 @@
+import fs from 'fs';
+const TOKEN=process.env.TOKEN, STORE="designer-laboratory-sandbox.myshopify.com", API="2024-10";
+const LIMIT=process.env.LIMIT?parseInt(process.env.LIMIT):999999;
+const ONLY=process.env.ONLY? process.env.ONLY.split(','):null;
+const url=`https://${STORE}/admin/api/${API}/graphql.json`;
+const gql=async(q,v)=>{const r=await fetch(url,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});return (await r.json());};
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const missing=JSON.parse(fs.readFileSync('zoffany-missing-sample.json'));
+const QP=`query($id:ID!){product(id:$id){variants(first:25){edges{node{id title sku price selectedOptions{name value}}}}}}`;
+const MU=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$variants){productVariants{id title sku} userErrors{field message}}}`;
+const MC=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkCreate(productId:$pid,variants:$variants){productVariants{id title sku price} userErrors{field message}}}`;
+let targets=missing;
+if(ONLY) targets=missing.filter(p=>ONLY.includes(String(p.id)));
+targets=targets.slice(0,LIMIT);
+const out=[];
+for(const p of targets){
+  const gid=p.gid; const rec={id:p.id,title:p.title};
+  try{
+    const cur=(await gql(QP,{id:gid})).data?.product?.variants?.edges?.map(e=>e.node)||[];
+    const roll=cur.find(v=>!/sample/i.test(v.title||''));
+    const hasSample=cur.some(v=>/sample/i.test(v.title||''));
+    if(hasSample){rec.status='already-has-sample';out.push(rec);continue;}
+    if(!roll||!roll.sku){rec.status='no-sellable-sku';out.push(rec);continue;}
+    const base=roll.sku.trim();
+    // Step 1: rename the sellable variant's Title option value -> "Sold Per Roll" (GID-preserving)
+    if((roll.title||'')==='Default Title'){
+      const u=await gql(MU,{pid:gid,variants:[{id:roll.id,optionValues:[{optionName:"Title",name:"Sold Per Roll"}]}]});
+      const ue=u.data?.productVariantsBulkUpdate?.userErrors||[];
+      if(ue.length){rec.status='rename-failed';rec.err=ue;out.push(rec);continue;}
+    }
+    // Step 2: add Sample
+    const c=await gql(MC,{pid:gid,variants:[{price:"4.25",optionValues:[{optionName:"Title",name:"Sample"}],inventoryItem:{sku:`${base}-Sample`,tracked:false},inventoryPolicy:"DENY"}]});
+    const ce=c.data?.productVariantsBulkCreate?.userErrors||[];
+    if(ce.length){rec.status='sample-create-failed';rec.err=ce;out.push(rec);continue;}
+    rec.status='ok';rec.base=base;rec.sample_gid=c.data.productVariantsBulkCreate.productVariants[0].id;
+    out.push(rec);
+  }catch(e){rec.status='exception';rec.err=String(e);out.push(rec);}
+  await sleep(300);
+}
+const ok=out.filter(r=>r.status==='ok').length;
+fs.writeFileSync(process.env.OUTFILE||'zof-sample-run-out.json',JSON.stringify(out,null,2));
+console.log('processed',out.length,'ok',ok);
+const byStatus={};out.forEach(r=>byStatus[r.status]=(byStatus[r.status]||0)+1);console.log(JSON.stringify(byStatus));
+out.filter(r=>r.status!=='ok').slice(0,10).forEach(r=>console.log('NONOK',r.id,r.status,JSON.stringify(r.err||'')));
diff --git a/shopify/scripts/zoffany-work/zof-vp.mjs b/shopify/scripts/zoffany-work/zof-vp.mjs
new file mode 100644
index 00000000..d879b015
--- /dev/null
+++ b/shopify/scripts/zoffany-work/zof-vp.mjs
@@ -0,0 +1,5 @@
+const TOKEN=process.env.TOKEN,STORE="designer-laboratory-sandbox.myshopify.com",API="2024-10";
+const url=`https://${STORE}/admin/api/${API}/graphql.json`;
+const gql=async(q,v)=>{const r=await fetch(url,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});return (await r.json()).data;};
+const d=await gql(`query($id:ID!){product(id:$id){title options{name values} variants(first:10){edges{node{id title sku price}}}}}`,{id:"gid://shopify/Product/7793953112115"});
+console.log(JSON.stringify(d.product,null,2));

← 2c253463 auto-data-snapshot: 2026-08-27T14:29:15 (1 data files) — pac  ·  back to Dw Validator Debug TK11314  ·  block sample prices from roll imports 3c6d03f6 →