[object Object]

← back to Designer Wallcoverings

Zoffany batch title normalization (2026-06-21): backtick->apostrophe + possessive case on 3 Marsden DWZF SKUs

daa2dccbacd6e591ee08ee0cfc140f0d13ff3871 · 2026-06-21 13:57:04 -0700 · SteveStudio2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit daa2dccbacd6e591ee08ee0cfc140f0d13ff3871
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Sun Jun 21 13:57:04 2026 -0700

    Zoffany batch title normalization (2026-06-21): backtick->apostrophe + possessive case on 3 Marsden DWZF SKUs
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .../zoffany-normalize-batch.js                     | 72 ++++++++++++++++++++++
 .../today-audit-2026-06-21/zoffany-pull-all.js     | 27 ++++++++
 .../zoffany-verify-source.js                       | 32 ++++++++++
 3 files changed, 131 insertions(+)

diff --git a/shopify/scripts/data/today-audit-2026-06-21/zoffany-normalize-batch.js b/shopify/scripts/data/today-audit-2026-06-21/zoffany-normalize-batch.js
new file mode 100644
index 00000000..bf17c7a0
--- /dev/null
+++ b/shopify/scripts/data/today-audit-2026-06-21/zoffany-normalize-batch.js
@@ -0,0 +1,72 @@
+// Batch title-normalization for the Zoffany (DWZF) products created TODAY 2026-06-21.
+// REVERSIBLE, TITLE-ONLY edits. Canonical format (decided from the live batch — 18/19
+// already follow it): "Pattern Name - Color Name <descriptor> | Zoffany"
+//   - descriptor = lowercase "wallcovering" (or "fabric" for productType Fabric)
+//   - separator " - ", suffix " | Zoffany", Title Case pattern + color.
+//
+// Only 3 of 19 ACTIVE need fixing — all backtick/apostrophe + possessive-case artifacts.
+// Color words left verbatim (custom.color/real_color_name are NULL across the batch, so
+// there is NO authoritative source to "correct" e.g. "Pale Poisen" — AI must never invent
+// color text). The other 16 ACTIVE + the already-cleaned DWZF-187374 are untouched, and
+// DWZF-187385 is ARCHIVED (out of scope).
+//
+// Each edit is gid+sku+before-pinned: the script re-reads the live title and ABORTS the
+// individual edit if the live "before" doesn't match exactly (no blind writes).
+// DRY-RUN by default; pass --apply to write. Canonical Shopify path/token reused.
+const https=require("https"),fs=require("fs"),os=require("os");
+const APPLY=process.argv.includes("--apply");
+const env=fs.readFileSync(os.homedir()+"/Projects/secrets-manager/.env","utf8");
+const TOKEN=(env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)||[])[1].replace(/['"]/g,"").trim();
+const STORE="designer-laboratory-sandbox.myshopify.com",API="2024-10";
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q,v={}){return new Promise((res,rej)=>{const b=JSON.stringify({query:q,variables:v});
+const r=https.request({host:STORE,path:`/admin/api/${API}/graphql.json`,method:"POST",headers:{"Content-Type":"application/json","X-Shopify-Access-Token":TOKEN,"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(d)}})});r.on("error",rej);r.write(b);r.end();});}
+async function gqlR(q,v={},t=5){for(let i=0;i<t;i++){let r;try{r=await gql(q,v)}catch(e){r={errors:[{message:String(e).slice(0,120)}]}}if(r&&!r.errors)return r;await sleep(1500*(i+1));}return gql(q,v);}
+const M=`mutation($input:ProductInput!){productUpdate(input:$input){product{id title}userErrors{field message}}}`;
+
+// HARD-PINNED edits (gid + sku + exact before, verified live via zoffany-pull-all.js)
+const EDITS=[
+ {sku:"DWZF-187377",gid:"gid://shopify/Product/7865417269299",
+  before:"Marsden`s Palm Damask - Blue Stone wallcovering | Zoffany",
+  after :"Marsden's Palm Damask - Blue Stone wallcovering | Zoffany",
+  reason:"backtick -> apostrophe"},
+ {sku:"DWZF-187378",gid:"gid://shopify/Product/7865417334835",
+  before:"Marsden`s Palm Damask - Pale Poisen wallcovering | Zoffany",
+  after :"Marsden's Palm Damask - Pale Poisen wallcovering | Zoffany",
+  reason:"backtick -> apostrophe (color 'Pale Poisen' left verbatim — no source to correct)"},
+ {sku:"DWZF-187379",gid:"gid://shopify/Product/7865417400371",
+  before:"Marsden`S Palm Damask - Tigers Eye wallcovering | Zoffany",
+  after :"Marsden's Palm Damask - Tigers Eye wallcovering | Zoffany",
+  reason:"backtick -> apostrophe + possessive S -> s"},
+];
+
+async function liveTitleByGid(gid){
+ const q=`query($id:ID!){product(id:$id){id title variants(first:1){nodes{sku}}}}`;
+ const r=await gqlR(q,{id:gid});
+ return r.data?.product||null;
+}
+
+(async()=>{
+ console.log(APPLY?"🔴 LIVE APPLY":"DRY-RUN (no writes)");
+ const results=[];
+ for(const e of EDITS){
+  const live=await liveTitleByGid(e.gid);
+  if(!live){console.log(`SKIP ${e.sku}: gid not found`);results.push({...e,outcome:"SKIP-not-found"});continue;}
+  const liveSku=live.variants?.nodes?.[0]?.sku;
+  if(liveSku!==e.sku){console.log(`SKIP ${e.sku}: gid maps to sku ${liveSku}`);results.push({...e,outcome:"SKIP-sku-mismatch"});continue;}
+  if(live.title===e.after){console.log(`ALREADY ${e.sku}: title already normalized`);results.push({...e,outcome:"ALREADY-DONE"});continue;}
+  if(live.title!==e.before){console.log(`SKIP ${e.sku}: live before mismatch\n   live=${JSON.stringify(live.title)}\n   exp =${JSON.stringify(e.before)}`);results.push({...e,outcome:"SKIP-before-mismatch",liveTitle:live.title});continue;}
+  console.log(`\n${e.sku}  (${e.reason})`);
+  console.log("  BEFORE:",JSON.stringify(e.before));
+  console.log("  AFTER :",JSON.stringify(e.after));
+  if(!APPLY){results.push({...e,outcome:"DRY-RUN"});continue;}
+  const r=await gqlR(M,{input:{id:e.gid,title:e.after}});
+  const ue=r.data?.productUpdate?.userErrors||[];
+  if(ue.length){console.log("  ERROR",JSON.stringify(ue));results.push({...e,outcome:"ERROR",userErrors:ue});continue;}
+  console.log("  OK ->",JSON.stringify(r.data.productUpdate.product.title));
+  results.push({...e,outcome:"APPLIED",newTitle:r.data.productUpdate.product.title});
+  await sleep(1500);
+ }
+ fs.writeFileSync("/tmp/zoffany-normalize-batch-result.json",JSON.stringify({applied:APPLY,ts:new Date().toISOString(),results},null,2));
+ console.log("\nResult JSON -> /tmp/zoffany-normalize-batch-result.json");
+})();
diff --git a/shopify/scripts/data/today-audit-2026-06-21/zoffany-pull-all.js b/shopify/scripts/data/today-audit-2026-06-21/zoffany-pull-all.js
new file mode 100644
index 00000000..8c6401a5
--- /dev/null
+++ b/shopify/scripts/data/today-audit-2026-06-21/zoffany-pull-all.js
@@ -0,0 +1,27 @@
+// Pull ALL Zoffany products created today (2026-06-21) with full id/status/title so we
+// can DECIDE the canonical title format from the batch before normalizing.
+// READ-ONLY. Canonical Shopify path/token reused from today-audit-2026-06-21 scripts.
+const https=require("https"),fs=require("fs"),os=require("os");
+const env=fs.readFileSync(os.homedir()+"/Projects/secrets-manager/.env","utf8");
+const TOKEN=(env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)||[])[1].replace(/['"]/g,"").trim();
+const STORE="designer-laboratory-sandbox.myshopify.com",API="2024-10";
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q,v={}){return new Promise((res,rej)=>{const b=JSON.stringify({query:q,variables:v});
+const r=https.request({host:STORE,path:`/admin/api/${API}/graphql.json`,method:"POST",headers:{"Content-Type":"application/json","X-Shopify-Access-Token":TOKEN,"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(d)}})});r.on("error",rej);r.write(b);r.end();});}
+async function gqlR(q,v={},t=5){for(let i=0;i<t;i++){let r;try{r=await gql(q,v)}catch(e){r={errors:[{message:String(e).slice(0,120)}]}}if(r&&!r.errors)return r;await sleep(1500*(i+1));}return gql(q,v);}
+(async()=>{
+ const out=[];let cursor=null;
+ while(true){
+  const q=`query($c:String){products(first:50,after:$c,query:"vendor:Zoffany created_at:>='2026-06-21'"){pageInfo{hasNextPage endCursor}
+   nodes{id legacyResourceId handle title status createdAt variants(first:1){nodes{sku}}}}}`;
+  const r=await gqlR(q,{c:cursor});
+  if(r.errors){console.error(JSON.stringify(r.errors).slice(0,300));break;}
+  const pg=r.data.products;
+  for(const n of pg.nodes)out.push({sku:n.variants.nodes[0]?.sku,gid:n.id,pid:n.legacyResourceId,handle:n.handle,status:n.status,title:n.title,createdAt:n.createdAt});
+  if(!pg.pageInfo.hasNextPage)break;cursor=pg.pageInfo.endCursor;
+ }
+ out.sort((a,b)=>(a.sku||"").localeCompare(b.sku||""));
+ fs.writeFileSync("/tmp/zoffany-pull-all.json",JSON.stringify(out,null,2));
+ console.log("Zoffany products created today:",out.length);
+ for(const p of out)console.log(`${p.status.padEnd(8)} ${p.sku||"(no-sku)"}  ${JSON.stringify(p.title)}`);
+})();
diff --git a/shopify/scripts/data/today-audit-2026-06-21/zoffany-verify-source.js b/shopify/scripts/data/today-audit-2026-06-21/zoffany-verify-source.js
new file mode 100644
index 00000000..e88c4d60
--- /dev/null
+++ b/shopify/scripts/data/today-audit-2026-06-21/zoffany-verify-source.js
@@ -0,0 +1,32 @@
+// READ-ONLY: pull source-of-truth metafields (real color, pattern, mfr sku, product type)
+// for the questionable Zoffany SKUs so title normalization uses scraped color, never AI.
+const https=require("https"),fs=require("fs"),os=require("os");
+const env=fs.readFileSync(os.homedir()+"/Projects/secrets-manager/.env","utf8");
+const TOKEN=(env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)||[])[1].replace(/['"]/g,"").trim();
+const STORE="designer-laboratory-sandbox.myshopify.com",API="2024-10";
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q,v={}){return new Promise((res,rej)=>{const b=JSON.stringify({query:q,variables:v});
+const r=https.request({host:STORE,path:`/admin/api/${API}/graphql.json`,method:"POST",headers:{"Content-Type":"application/json","X-Shopify-Access-Token":TOKEN,"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(d)}})});r.on("error",rej);r.write(b);r.end();});}
+async function gqlR(q,v={},t=5){for(let i=0;i<t;i++){let r;try{r=await gql(q,v)}catch(e){r={errors:[{message:String(e).slice(0,120)}]}}if(r&&!r.errors)return r;await sleep(1500*(i+1));}return gql(q,v);}
+const SKUS=process.argv.slice(2).length?process.argv.slice(2):
+ ["DWZF-187045","DWZF-187377","DWZF-187378","DWZF-187379","DWZF-187372","DWZF-187036"];
+(async()=>{
+ const out=[];
+ for(const sku of SKUS){
+  const q=`query($q:String!){productVariants(first:5,query:$q){nodes{sku product{legacyResourceId handle title productType status
+   metafields(first:80){nodes{namespace key value}}}}}}`;
+  const r=await gqlR(q,{q:`sku:${sku}`});
+  const vn=r.data?.productVariants?.nodes||[];
+  const m=vn.find(v=>v.sku===sku)||vn[0];
+  if(!m){out.push({sku,FOUND:false});continue;}
+  const p=m.product;const mf={};
+  for(const x of p.metafields.nodes)mf[`${x.namespace}.${x.key}`]=x.value;
+  out.push({sku,title:p.title,productType:p.productType,status:p.status,
+   real_color:mf["custom.real_color_name"]||null,color_custom:mf["custom.color"]||null,
+   colorway_global:mf["global.Color-Way"]||null,color_dwc:mf["dwc.color"]||null,
+   pattern_dwc:mf["dwc.pattern_name"]||null,pattern_global:mf["global.Pattern"]||null,
+   mfr_sku:mf["custom.manufacturer_sku"]||mf["dwc.manufacturer_sku"]||null});
+ }
+ fs.writeFileSync("/tmp/zoffany-verify-source.json",JSON.stringify(out,null,2));
+ console.log(JSON.stringify(out,null,2));
+})();

← 959ae442 Archive confirmed-duplicate Zoffany DWZF-187385 (-1 dup-hand  ·  back to Designer Wallcoverings  ·  activate-gate: push-then-activate width metafield + apply ga 85ce69fb →