[object Object]

← back to Japan Enrich

TK-10649 remediation executors: M2 Novasuede images, M4-B Freshwater Grass mis-key fix, M1 mfr-backfill (dry-run staged)

bf1295dce166790c0bc4f21bb67940a5075689bb · 2026-08-18 09:08:21 -0700 · Steve Abrams

Files touched

Diff

commit bf1295dce166790c0bc4f21bb67940a5075689bb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 09:08:21 2026 -0700

    TK-10649 remediation executors: M2 Novasuede images, M4-B Freshwater Grass mis-key fix, M1 mfr-backfill (dry-run staged)
---
 tk10649-m1-mfr-backfill.js     | 99 ++++++++++++++++++++++++++++++++++++++++++
 tk10649-m2-novasuede-images.js | 72 ++++++++++++++++++++++++++++++
 tk10649-m4b-freshwater-fix.js  | 71 ++++++++++++++++++++++++++++++
 3 files changed, 242 insertions(+)

diff --git a/tk10649-m1-mfr-backfill.js b/tk10649-m1-mfr-backfill.js
new file mode 100644
index 0000000..a0947cd
--- /dev/null
+++ b/tk10649-m1-mfr-backfill.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+// TK-10649 M1 — COALESCE-guarded, FILL-EMPTY-ONLY mfr backfill for the 1,257
+// published+sellable ACTIVE products with NO mfr sku anywhere.
+// DRY_RUN=1 (default) => report only, NO writes.  DRY_RUN=0 => fire (Steve-gated GO).
+//
+// HARD RAILS baked in:
+//  - fill-empty ONLY: never touches a product that already has ANY mfr (column or either metafield)
+//  - joins per-vendor on the RELIABLE key found in the audit (shopify_id for Zoffany;
+//    Tres Tintas/Scalamandre/Sister Parish have NO reliable staging join → reported, NOT written)
+//  - Malibu is PRIVATE-LABEL (WallQuest->Malibu): its staging "mfr_sku" is a slug that can
+//    LEAK the real product identity → held out of the auto-backfill (flagged for owner)
+//  - writes BOTH the top-level mirror mfr_sku (local read) AND the Shopify dwc.manufacturer_sku
+//    metafield (customer-facing authoritative); Shopify write is the source of truth
+//
+// This file is the executor; the DRY-RUN report is what goes to Steve for the GO.
+
+const fs = require('fs');
+const { execSync } = require('child_process');
+const https = require('https');
+const path = require('path');
+
+for (const l of fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8').split('\n')) {
+  const m = l.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
+  if (m && !(m[1] in process.env)) process.env[m[1]] = m[2];
+}
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const DRY = process.env.DRY_RUN !== '0';
+const PSQL = 'psql -h /tmp -d dw_unified -tAc';
+
+// Per-vendor backfill plan derived from the 2026-08-18 join-key audit.
+// key = the ONLY reliable join found; null => no reliable staging join (report, do not write).
+const PLAN = [
+  { vendor: 'Zoffany',          table: 'zoffany_catalog',      key: 'shopify_id', backfillable: true  },
+  { vendor: 'Tres Tintas',      table: 'tres_tintas_catalog',  key: null,         backfillable: false, reason: 'live dw_sku empty + DWTB variant-prefix vs staging DWTS dw_sku; staging shopify_product_id empty → 0 matches by any key' },
+  { vendor: 'Scalamandre',      table: 'scalamandre_catalog',  key: null,         backfillable: false, reason: '0 matches by dw_sku / variant_sku / shopify_id' },
+  { vendor: 'Sister Parish',    table: 'sisterparish_catalog', key: null,         backfillable: false, reason: 'staging has NO mfr_sku column (Shopify-mirror scrape); private-label DWPH line' },
+  { vendor: 'Malibu Wallpaper', table: 'malibu_catalog',       key: 'shopify_id', backfillable: false, reason: 'PRIVATE-LABEL (WallQuest->Malibu): staging mfr_sku is a real-vendor slug that would LEAK the true product identity customer-facing — owner call' },
+];
+
+function q(sql) { return execSync(`${PSQL} "${sql.replace(/"/g, '\\"')}"`, { encoding: 'utf8' }).trim(); }
+
+function gql(query, variables) {
+  return new Promise((res, rej) => {
+    const b = JSON.stringify({ query, variables });
+    const r = https.request({ host: DOMAIN, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(b) } },
+      x => { let d = ''; x.on('data', c => d += c); x.on('end', () => { try { res(JSON.parse(d)); } catch (e) { rej(new Error(d)); } }); });
+    r.on('error', rej); r.write(b); r.end();
+  });
+}
+
+const NO_MFR = `(mfr_sku IS NULL OR mfr_sku='') AND COALESCE(metafields->'dwc'->'manufacturer_sku'->>'value','')='' AND COALESCE(metafields->'custom'->'manufacturer_sku'->>'value','')=''`;
+
+(async () => {
+  console.log(`\n=== TK-10649 M1 — mfr backfill (${DRY ? 'DRY-RUN (report only)' : 'LIVE'}) ===\n`);
+  let totalBackfillable = 0, totalBlocked = 0;
+  const fireList = [];
+  for (const p of PLAN) {
+    const total = +q(`SELECT count(*) FROM shopify_products WHERE status='ACTIVE' AND online_store_published IS TRUE AND has_product_variant IS TRUE AND vendor='${p.vendor}' AND ${NO_MFR}`);
+    if (!p.backfillable) {
+      totalBlocked += total;
+      console.log(`[BLOCKED] ${p.vendor}: ${total} no-mfr — ${p.reason}`);
+      continue;
+    }
+    // Zoffany-style: join staging by shopify_id, mfr_sku real & non-empty
+    const rows = q(`SELECT regexp_replace(l.shopify_id,'.*/','')||'|'||s.mfr_sku
+       FROM shopify_products l JOIN ${p.table} s ON s.shopify_product_id::text=regexp_replace(l.shopify_id,'.*/','')
+       WHERE l.status='ACTIVE' AND l.online_store_published IS TRUE AND l.has_product_variant IS TRUE AND l.vendor='${p.vendor}'
+         AND ${NO_MFR.replace(/mfr_sku/g,'l.mfr_sku').replace(/metafields/g,'l.metafields')}
+         AND s.mfr_sku IS NOT NULL AND s.mfr_sku<>''`).split('\n').filter(Boolean);
+    totalBackfillable += rows.length;
+    console.log(`[OK] ${p.vendor}: ${total} no-mfr → ${rows.length} backfillable via ${p.key}`);
+    for (const r of rows) { const [sid, mfr] = r.split('|'); fireList.push({ vendor: p.vendor, sid, mfr }); }
+  }
+  console.log(`\n=== TOTALS ===`);
+  console.log(`  backfillable NOW (reliable join): ${totalBackfillable}`);
+  console.log(`  BLOCKED (no reliable join / private-label leak risk): ${totalBlocked}`);
+  console.log(`  fireList size: ${fireList.length}`);
+
+  if (DRY) { console.log(`\n[dry-run] no writes. fireList staged. Await Steve GO for DRY_RUN=0.\n`); fs.writeFileSync('/tmp/tk10649-m1-firelist.json', JSON.stringify(fireList, null, 2)); return; }
+
+  // LIVE: fill-empty only — set mirror mfr_sku + Shopify dwc.manufacturer_sku
+  let done = 0;
+  for (const f of fireList) {
+    const gid = `gid://shopify/Product/${f.sid}`;
+    // guard: re-read live, skip if any mfr already present
+    const cur = await gql(`{ product(id:"${gid}"){ mfrCol: metafield(namespace:"dwc",key:"manufacturer_sku"){ value } cust: metafield(namespace:"custom",key:"manufacturer_sku"){ value } } }`);
+    const pm = cur.data && cur.data.product;
+    if (pm && ((pm.mfrCol && pm.mfrCol.value) || (pm.cust && pm.cust.value))) { console.log(`  SKIP ${f.sid} already has mfr`); continue; }
+    await gql(`mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{ message } } }`,
+      { mf: [{ ownerId: gid, namespace: 'dwc', key: 'manufacturer_sku', type: 'single_line_text_field', value: f.mfr }] });
+    // mirror column fill-empty
+    execSync(`${PSQL} "UPDATE shopify_products SET mfr_sku='${f.mfr.replace(/'/g,"''")}' WHERE regexp_replace(shopify_id,'.*/','')='${f.sid}' AND (mfr_sku IS NULL OR mfr_sku='')"`);
+    done++;
+    if (done % 10 === 0) console.log(`  ...${done}/${fireList.length}`);
+  }
+  console.log(`\nLIVE backfill complete: ${done} products.`);
+})();
diff --git a/tk10649-m2-novasuede-images.js b/tk10649-m2-novasuede-images.js
new file mode 100644
index 0000000..11046e0
--- /dev/null
+++ b/tk10649-m2-novasuede-images.js
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+// TK-10649 M2: set the already-existing catalog image as featured media on the
+// 3 imageless ACTIVE Novasuede products (Java/Indigo/Black).
+// DRY_RUN=1 (default) => plan only.  DRY_RUN=0 => fire productCreateMedia.
+// Reversible: adds a media item; no delete, no overwrite of a populated image
+// (guard: skip if the live product already has media).
+
+const fs = require('fs');
+const https = require('https');
+const path = require('path');
+
+function loadEnv(p) {
+  for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
+    const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
+    if (m && !(m[1] in process.env)) process.env[m[1]] = m[2];
+  }
+}
+loadEnv(path.join(process.env.HOME, 'Projects/secrets-manager/.env'));
+
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const DRY = process.env.DRY_RUN !== '0';
+
+// mirror-verified: shopify_id -> {title, image_url from novasuede_catalog by mfr_sku}
+const TARGETS = [
+  { pid: '7692851937331', color: 'Java',   img: 'https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-java.jpg?v=1761251983', sellable: true  },
+  { pid: '7692851839027', color: 'Indigo', img: 'https://novasuede.com/images/colors/novasuede-indigo.jpg',                                  sellable: false },
+  { pid: '7692850298931', color: 'Black',  img: 'https://novasuede.com/images/colors/novasuede-black.jpg',                                   sellable: false },
+];
+
+function gql(query) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query });
+    const req = https.request({
+      host: DOMAIN, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
+    }, (r) => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(d)); } }); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+
+(async () => {
+  console.log(`\n=== TK-10649 M2 — Novasuede featured-image restore (${DRY ? 'DRY-RUN' : 'LIVE'}) ===\n`);
+  const results = [];
+  for (const t of TARGETS) {
+    const gid = `gid://shopify/Product/${t.pid}`;
+    // Step 0: live media check (skip if already has media)
+    const cur = await gql(`{ product(id:"${gid}"){ id title featuredImage{url} media(first:5){nodes{ id mediaContentType status }} } }`);
+    const p = cur.data && cur.data.product;
+    if (!p) { results.push({ color: t.color, status: 'ERROR_read', detail: JSON.stringify(cur).slice(0, 200) }); continue; }
+    const mediaCount = p.media.nodes.length;
+    const hasFeatured = !!(p.featuredImage && p.featuredImage.url);
+    console.log(`[${t.color}] ${p.title}`);
+    console.log(`  live BEFORE: featured=${hasFeatured ? p.featuredImage.url : 'NONE'} | media=${mediaCount} | sellable=${t.sellable}`);
+    console.log(`  will SET featured  -> ${t.img}`);
+    if (hasFeatured || mediaCount > 0) {
+      console.log(`  SKIP: live already has media (guard) — no write.\n`);
+      results.push({ color: t.color, status: 'SKIP_has_media' });
+      continue;
+    }
+    if (DRY) { console.log(`  [dry-run] would productCreateMedia\n`); results.push({ color: t.color, status: 'PLANNED' }); continue; }
+    const mut = await gql(`mutation { productCreateMedia(productId:"${gid}", media:[{ originalSource:"${t.img}", mediaContentType: IMAGE, alt:"Novasuede ${t.color}" }]){ media{ ... on MediaImage{ id status } } mediaUserErrors{ field message } userErrors{ field message } } }`);
+    const mres = mut.data && mut.data.productCreateMedia;
+    const errs = [...(mres && mres.mediaUserErrors || []), ...(mres && mres.userErrors || [])];
+    if (errs.length) { console.log(`  ERROR: ${JSON.stringify(errs)}\n`); results.push({ color: t.color, status: 'ERROR_write', errs }); continue; }
+    const mid = mres.media && mres.media[0] && mres.media[0].id;
+    console.log(`  FIRED: media ${mid} status=${mres.media[0] && mres.media[0].status}\n`);
+    results.push({ color: t.color, status: 'FIRED', media_id: mid });
+  }
+  console.log('=== SUMMARY ===');
+  for (const r of results) console.log(`  ${r.color}: ${r.status}${r.media_id ? ' ' + r.media_id : ''}`);
+})();
diff --git a/tk10649-m4b-freshwater-fix.js b/tk10649-m4b-freshwater-fix.js
new file mode 100644
index 0000000..a614efa
--- /dev/null
+++ b/tk10649-m4b-freshwater-fix.js
@@ -0,0 +1,71 @@
+#!/usr/bin/env node
+// TK-10649 M4-B (DTD verdict X, 5/5): fix the mis-keyed DW-house "Freshwater Grass"
+// row (gid 7432702033971) — the ONLY unambiguously-wrong customer-facing data.
+//   1) custom.manufacturer_sku  WP30020 (MTG's Grandma's-Tapestry code) -> WP20732 (real Freshwater Grass code)
+//   2) dwc.manufacturer_sku     null/wrong -> WP20732 (align)
+//   3) body_html  copied Chinoiserie text -> the ACCURATE Freshwater Grass copy,
+//      taken VERBATIM from the genuine Freshwater Grass product DWC-268059 (NOT AI-generated)
+// Collision A (Arte+Koroseal DWK-32014) gets NO write — legitimate mfr/distributor
+// relationship, live variant SKUs already differ (DTD X).
+// DRY_RUN=1 (default) => plan only.  DRY_RUN=0 => fire.  All reversible field writes.
+
+const fs = require('fs');
+const https = require('https');
+const path = require('path');
+
+for (const l of fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8').split('\n')) {
+  const m = l.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
+  if (m && !(m[1] in process.env)) process.env[m[1]] = m[2];
+}
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const DRY = process.env.DRY_RUN !== '0';
+
+const GID = 'gid://shopify/Product/7432702033971';       // DW-house Freshwater Grass (mis-keyed)
+const REAL_MFR = 'WP20732';                               // the genuine Freshwater Grass code (from DWC-268059)
+const WRONG_MFR = 'WP30020';                              // MTG Grandma's Tapestry code (does not belong here)
+const CORRECT_BODY = '<p>This wallcovering features a dense arrangement of freshwater grass in shades of blue, green, ochre, and turquoise. It evokes a tropical and organic mood, bringing a touch of nature indoors.</p>';
+
+function gql(query, variables) {
+  return new Promise((res, rej) => {
+    const b = JSON.stringify({ query, variables });
+    const r = https.request({ host: DOMAIN, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(b) } },
+      x => { let d = ''; x.on('data', c => d += c); x.on('end', () => { try { res(JSON.parse(d)); } catch (e) { rej(new Error(d)); } }); });
+    r.on('error', rej); r.write(b); r.end();
+  });
+}
+const strip = s => (s || '').replace(/<[^>]+>/g, ' ').trim().slice(0, 140);
+
+(async () => {
+  console.log(`\n=== TK-10649 M4-B — Freshwater Grass mis-key fix (${DRY ? 'DRY-RUN' : 'LIVE'}) ===\n`);
+  const cur = await gql(`{ product(id:"${GID}"){ title bodyHtml
+      customMfr: metafield(namespace:"custom", key:"manufacturer_sku"){ id value }
+      dwcMfr: metafield(namespace:"dwc", key:"manufacturer_sku"){ id value } } }`);
+  const p = cur.data.product;
+  console.log('title:', p.title);
+  console.log('\ncustom.manufacturer_sku BEFORE:', JSON.stringify(p.customMfr && p.customMfr.value), '  AFTER:', JSON.stringify(REAL_MFR));
+  console.log('dwc.manufacturer_sku    BEFORE:', JSON.stringify(p.dwcMfr && p.dwcMfr.value), '  AFTER:', JSON.stringify(REAL_MFR));
+  console.log('body_html BEFORE:', JSON.stringify(strip(p.bodyHtml)));
+  console.log('body_html AFTER :', JSON.stringify(strip(CORRECT_BODY)), '(verbatim from genuine DWC-268059)');
+
+  // Safety guard: only proceed if the wrong code is actually present
+  if (!(p.customMfr && p.customMfr.value === WRONG_MFR)) {
+    console.log('\nGUARD: custom.manufacturer_sku is not the expected WRONG code (' + WRONG_MFR + '). Aborting to avoid clobbering. Current=' + JSON.stringify(p.customMfr && p.customMfr.value));
+    return;
+  }
+  if (DRY) { console.log('\n[dry-run] would set 2 metafields + productUpdate body_html\n'); return; }
+
+  // 1+2) metafields
+  const mf = await gql(`mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ metafields{ namespace key value } userErrors{ field message } } }`,
+    { mf: [
+      { ownerId: GID, namespace: 'custom', key: 'manufacturer_sku', type: 'single_line_text_field', value: REAL_MFR },
+      { ownerId: GID, namespace: 'dwc',    key: 'manufacturer_sku', type: 'single_line_text_field', value: REAL_MFR },
+    ] });
+  console.log('\nmetafieldsSet:', JSON.stringify(mf.data.metafieldsSet.userErrors.length ? mf.data.metafieldsSet.userErrors : mf.data.metafieldsSet.metafields));
+
+  // 3) body_html
+  const upd = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id } userErrors{ field message } } }`,
+    { input: { id: GID, bodyHtml: CORRECT_BODY } });
+  console.log('productUpdate body:', JSON.stringify(upd.data.productUpdate.userErrors.length ? upd.data.productUpdate.userErrors : 'OK'));
+})();

(oldest)  ·  back to Japan Enrich  ·  feat: Carnegie split rollout gate — import canonical mfr_sku 1609532 →