← back to Designerwallcoverings
TK-11644: description_tag mass-overwrite — sweep, proof, and gated executor
1d0b9dd679110746bcc57719fd70315b3939cbad · 2026-09-13 16:34:03 -0700 · Steve Abrams
The 2026-04-05T23:20-23:26Z job that corrupted title_tag (TK-11564) also overwrote
33,198 ACTIVE meta descriptions with generic store boilerplate. Different corruption
shape than title_tag (genericization, not wrong-identity), which is why identity-based
checks read it as clean.
Proof: metafield.createdAt partition -> created-by-job=0, overwrote-pre-existing=33,198
(100%), createdAt spanning 2020-04..2026-02. That spread also proves the test is valid
rather than void: a delete+recreate job would have stamped every createdAt in-window.
Same-vendor control: Coordonne rows the job missed are 97.5% product-specific; the 1,019
it touched are 0.2%.
Fallback gate measured BEFORE proposing the fix: 99.9% of affected rows have a usable
body_html; pattern-specificity goes 3.8% -> 30.2%. The 41 unsafe rows are excluded.
tk11644-desctag-sweep.mjs catalog-wide window sweep (read-only)
tk11644-createdat-partition.mjs backfill-vs-overwrite proof (read-only)
tk11644-fallback-quality.mjs body_html fallback safety gate (read-only)
tk11644-desctag-fix.mjs phased executor, DRY-RUN BY DEFAULT, race-safe
No Shopify write fired. Remediation drafted to pending-approval for Steve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ch7HhNSTbzTTBS4rKmk5j
Files touched
A scripts/tk11644-createdat-partition.mjsA scripts/tk11644-desctag-fix.mjsA scripts/tk11644-desctag-sweep.mjsA scripts/tk11644-fallback-quality.mjs
Diff
commit 1d0b9dd679110746bcc57719fd70315b3939cbad
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:34:03 2026 -0700
TK-11644: description_tag mass-overwrite — sweep, proof, and gated executor
The 2026-04-05T23:20-23:26Z job that corrupted title_tag (TK-11564) also overwrote
33,198 ACTIVE meta descriptions with generic store boilerplate. Different corruption
shape than title_tag (genericization, not wrong-identity), which is why identity-based
checks read it as clean.
Proof: metafield.createdAt partition -> created-by-job=0, overwrote-pre-existing=33,198
(100%), createdAt spanning 2020-04..2026-02. That spread also proves the test is valid
rather than void: a delete+recreate job would have stamped every createdAt in-window.
Same-vendor control: Coordonne rows the job missed are 97.5% product-specific; the 1,019
it touched are 0.2%.
Fallback gate measured BEFORE proposing the fix: 99.9% of affected rows have a usable
body_html; pattern-specificity goes 3.8% -> 30.2%. The 41 unsafe rows are excluded.
tk11644-desctag-sweep.mjs catalog-wide window sweep (read-only)
tk11644-createdat-partition.mjs backfill-vs-overwrite proof (read-only)
tk11644-fallback-quality.mjs body_html fallback safety gate (read-only)
tk11644-desctag-fix.mjs phased executor, DRY-RUN BY DEFAULT, race-safe
No Shopify write fired. Remediation drafted to pending-approval for Steve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ch7HhNSTbzTTBS4rKmk5j
---
scripts/tk11644-createdat-partition.mjs | 53 +++++++++++++++++
scripts/tk11644-desctag-fix.mjs | 77 ++++++++++++++++++++++++
scripts/tk11644-desctag-sweep.mjs | 102 ++++++++++++++++++++++++++++++++
scripts/tk11644-fallback-quality.mjs | 46 ++++++++++++++
4 files changed, 278 insertions(+)
diff --git a/scripts/tk11644-createdat-partition.mjs b/scripts/tk11644-createdat-partition.mjs
new file mode 100644
index 0000000..caee599
--- /dev/null
+++ b/scripts/tk11644-createdat-partition.mjs
@@ -0,0 +1,53 @@
+// TK-11644 READ-ONLY decisive partition: did the 2026-04-05 job CREATE each description_tag
+// (additive backfill — nothing destroyed) or OVERWRITE a pre-existing one (real destruction)?
+// metafield.createdAt is preserved by an upsert (metafieldsSet) but NOT by delete+recreate,
+// so we also report the createdAt distribution to prove the test isn't void.
+// No writes. $0.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+const Q=`{ products { edges { node { id handle vendor title status
+ metafield(namespace:"global",key:"description_tag"){ value createdAt updatedAt } } } } }`;
+const RAW='assets/TK-11644-createdat-raw.jsonl';
+let jsonl;
+if(fs.existsSync(RAW)&&process.env.USE_CACHE==='1'){ jsonl=fs.readFileSync(RAW,'utf8'); }
+else{
+ const m=await gql(`mutation { bulkOperationRunQuery(query: ${JSON.stringify(Q)}) { bulkOperation{id status} userErrors{message} } }`);
+ if(m.bulkOperationRunQuery.userErrors.length){console.error(m.bulkOperationRunQuery.userErrors);process.exit(1);}
+ for(;;){ const d=await gql(`{ currentBulkOperation(type:QUERY){ status objectCount url } }`);
+ const b=d.currentBulkOperation; process.stderr.write(`\r bulk ${b.status} objs=${b.objectCount} `);
+ if(b.status==='COMPLETED'){process.stderr.write('\n'); jsonl=await (await fetch(b.url)).text(); break;}
+ if(['FAILED','CANCELED'].includes(b.status)){console.error(b);process.exit(1);}
+ await new Promise(r=>setTimeout(r,3000)); }
+ fs.writeFileSync(RAW,jsonl);
+}
+const LO=new Date('2026-04-05T23:20:00Z').getTime(), HI=new Date('2026-04-05T23:27:00Z').getTime();
+const BOIL="Our professional sales staff at Designer Wallcoverings";
+let created=0, overwrote=0, createdAfter=0; const cHist={}; const ow=[];
+const spec=(t,d)=>{const p=t.split(' | ')[0].split(' - ')[0].trim().toLowerCase(); return p.length>=6 && d.toLowerCase().includes(p);};
+let owBoil=0, crBoil=0;
+for(const l of jsonl.split('\n')){ if(!l)continue; const n=JSON.parse(l);
+ if(!n.id?.includes('/Product/')||n.status!=='ACTIVE') continue;
+ const mf=n.metafield; if(!mf?.updatedAt) continue;
+ const u=new Date(mf.updatedAt).getTime(); if(u<LO||u>HI) continue; // in-window ACTIVE only
+ const c=new Date(mf.createdAt).getTime();
+ cHist[mf.createdAt.slice(0,7)]=(cHist[mf.createdAt.slice(0,7)]||0)+1;
+ const v=(mf.value||'').trim();
+ if(c>=LO&&c<=HI){ created++; if(v.startsWith(BOIL))crBoil++; }
+ else if(c<LO){ overwrote++; if(v.startsWith(BOIL))owBoil++;
+ ow.push({handle:n.handle,vendor:(n.vendor||'').trim(),title:n.title,createdAt:mf.createdAt,updatedAt:mf.updatedAt,value:v}); }
+ else createdAfter++;
+}
+const tot=created+overwrote+createdAfter;
+console.log(JSON.stringify({
+ in_window_ACTIVE: tot,
+ CREATED_by_job_additive_backfill: created,
+ OVERWROTE_preexisting_value: overwrote,
+ created_after_window_anomaly: createdAfter,
+ pct_overwrite: +(100*overwrote/tot).toFixed(1),
+ boilerplate_among_OVERWRITTEN: owBoil,
+ boilerplate_among_CREATED: crBoil,
+ createdAt_month_histogram: Object.entries(cHist).sort(),
+ test_validity_note: created===tot ? 'VOID - all createdAt in-window, job likely delete+recreate' : 'VALID - createdAt varies, upsert preserved provenance',
+},null,2));
+fs.writeFileSync('assets/TK-11644-overwritten.json',JSON.stringify(ow,null,2));
+console.log('overwritten rows ->','assets/TK-11644-overwritten.json','count',ow.length);
diff --git a/scripts/tk11644-desctag-fix.mjs b/scripts/tk11644-desctag-fix.mjs
new file mode 100644
index 0000000..fcb84d3
--- /dev/null
+++ b/scripts/tk11644-desctag-fix.mjs
@@ -0,0 +1,77 @@
+// TK-11644 phased description_tag corruption FIX.
+// The 2026-04-05T23:20-23:26Z bulk job OVERWROTE 33,198 pre-existing ACTIVE meta descriptions
+// (proven: metafield.createdAt predates the job on 100% of them), replacing 19,053 with one
+// identical store-generic blurb. Fix = DELETE global.description_tag so Shopify falls back to
+// the product's own body_html excerpt (measured usable on 99.9%, ~8x more product-specific).
+//
+// DRY-RUN BY DEFAULT. --apply is the GATED live customer-facing write (Steve go only).
+// Race-safe: before each delete it LIVE-verifies (a) the metafield still holds the exact
+// corrupted value from the sweep — so a manual fix made since is never clobbered, and
+// (b) the body_html fallback is still usable — so we never expose an empty description.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+const args=process.argv.slice(2);
+const TIER=(args.find(a=>a.startsWith('--tier='))||'--tier=A').split('=')[1]; // A|B|AB
+const APPLY=args.includes('--apply');
+const LIMIT=parseInt((args.find(a=>a.startsWith('--limit='))||'--limit=0').split('=')[1],10);
+
+const load=t=>JSON.parse(fs.readFileSync(`assets/TK-11644-tier-${t}.json`,'utf8')).map(r=>({...r,tier:t}));
+let set = TIER==='AB' ? [...load('A'),...load('B')] : load(TIER);
+if(LIMIT>0) set=set.slice(0,LIMIT);
+console.error(`Tier=${TIER} candidates=${set.length} mode=${APPLY?'APPLY (LIVE WRITE)':'DRY-RUN'}`);
+
+// resolve gids + live state in batches of 100 (handle -> product)
+const verified=[], changed=[], badFallback=[], missing=[];
+for(let i=0;i<set.length;i+=100){
+ const chunk=set.slice(i,i+100);
+ const parts=chunk.map((r,j)=>`p${j}: productByHandle(handle:${JSON.stringify(r.handle)}){ id title status descriptionHtml metafield(namespace:"global",key:"description_tag"){ id value } }`);
+ let d; try{ d=await gql(`{ ${parts.join('\n')} }`); }catch(e){ console.error('batch error',e.message); continue; }
+ chunk.forEach((r,j)=>{
+ const n=d[`p${j}`];
+ if(!n){ missing.push(r.handle); return; }
+ const cur=(n.metafield?.value||'').trim();
+ if(!cur || cur!==r.value.trim()){ changed.push({handle:r.handle,now:cur.slice(0,60)||'(deleted)'}); return; }
+ const body=(n.descriptionHtml||'').replace(/<[^>]+>/g,' ').replace(/&[a-z#0-9]+;/gi,' ').replace(/\s+/g,' ').trim();
+ if(body.length<60){ badFallback.push({handle:r.handle,body_len:body.length}); return; }
+ verified.push({gid:n.id,pid:n.id.split('/').pop(),handle:r.handle,vendor:r.vendor,tier:r.tier,
+ old_value:cur,fallback:body.slice(0,160)});
+ });
+ process.stderr.write(`\r verified ${verified.length} changed-since-sweep ${changed.length} bad-fallback ${badFallback.length} missing ${missing.length} `);
+}
+process.stderr.write('\n');
+
+const mpath=`assets/TK-11644-fix-tier${TIER}-manifest.json`;
+fs.writeFileSync(mpath,JSON.stringify({tier:TIER,generated_at:new Date().toISOString(),
+ will_delete:verified.length,skipped_changed:changed.length,skipped_bad_fallback:badFallback.length,
+ skipped_missing:missing.length,rows:verified,changed,badFallback,missing},null,1));
+
+console.log(`\nWILL DELETE description_tag on: ${verified.length}`);
+console.log(`SKIP (changed since sweep): ${changed.length} (never clobber a manual fix)`);
+console.log(`SKIP (body fallback <60 chars): ${badFallback.length} (would expose an empty description)`);
+console.log(`SKIP (product not found): ${missing.length}`);
+console.log('\nSample before -> after (what Google will show instead):');
+for(const v of verified.slice(0,5)) console.log(` [${v.tier}] ${v.handle}\n was: ${JSON.stringify(v.old_value.slice(0,78))}\n ->: ${JSON.stringify(v.fallback.slice(0,78))}`);
+console.log('\nmanifest + rollback map ->',mpath);
+if(!APPLY){ console.log('\nDRY-RUN. No write fired. Re-run with --apply (GATED — Steve go only).'); process.exit(0); }
+
+// ---- LIVE WRITE PATH (gated) ----
+console.error('APPLYING: deleting description_tag on',verified.length,'products ...');
+const ledger=process.env.HOME+'/.claude/yolo-queue/executed-reversible/ledger.jsonl';
+let ok=0,err=0;
+for(let i=0;i<verified.length;i+=25){
+ const chunk=verified.slice(i,i+25);
+ const mf=chunk.map(v=>({ownerId:v.gid,namespace:'global',key:'description_tag'}));
+ const d=await gql(`mutation($mf:[MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$mf){ deletedMetafields{ ownerId } userErrors{ field message } } }`,{mf});
+ const ue=d.metafieldsDelete.userErrors;
+ if(ue.length){ console.error('userErrors',ue); err+=chunk.length; continue; }
+ ok+=d.metafieldsDelete.deletedMetafields.length;
+ for(const v of chunk) fs.appendFileSync(ledger,JSON.stringify({ts:new Date().toISOString(),
+ agent:'claude-run-11644',ticket:'TK-11644',action:`delete global.description_tag (tier ${v.tier})`,
+ pid:v.pid,handle:v.handle,blast_radius:1,
+ undo_cmd:`re-set global.description_tag=${JSON.stringify(v.old_value)} on product ${v.pid}`,
+ old_value:v.old_value,verify:`GET product ${v.pid} description_tag == null`})+'\n');
+ process.stderr.write(`\r deleted ${ok}/${verified.length} err ${err} `);
+}
+process.stderr.write('\n');
+console.log(`APPLY done. deleted=${ok} err=${err}. Rollback map ${mpath} + executed-reversible ledger.`);
diff --git a/scripts/tk11644-desctag-sweep.mjs b/scripts/tk11644-desctag-sweep.mjs
new file mode 100644
index 0000000..8fdd718
--- /dev/null
+++ b/scripts/tk11644-desctag-sweep.mjs
@@ -0,0 +1,102 @@
+// TK-11644 READ-ONLY blast-radius sweep: find every product whose global.description_tag
+// (SEO meta description) metafield was written by the SAME 2026-04-05 16:20-16:26 PT bad
+// bulk SEO job that corrupted global.title_tag (TK-11558 -> TK-11564, now remediated).
+//
+// AUTHORITATIVE SIGNAL = metafield updatedAt inside the job window. This is the exhaustive
+// method TK-11564's worker-w01 flagged as the missing backstop: it catches ANY value the job
+// wrote regardless of phrasing, instead of guessing text signatures first.
+// No writes. $0 read-only. Caches the raw bulk JSONL so re-analysis needs no re-export.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+const WIN_LO = new Date('2026-04-05T16:20:00-07:00').getTime();
+const WIN_HI = new Date('2026-04-05T16:27:00-07:00').getTime(); // padded 1min past last-seen 16:25:32
+const OUT = 'assets/TK-11644-desctag-sweep';
+const RAW = `${OUT}-raw.jsonl`;
+
+const BULK_QUERY = `{
+ products {
+ edges { node {
+ id handle vendor title status
+ metafield(namespace:"global", key:"description_tag") { value updatedAt }
+ } }
+ }
+}`;
+
+async function startBulk() {
+ const q = `mutation { bulkOperationRunQuery(query: ${JSON.stringify(BULK_QUERY)}) {
+ bulkOperation { id status } userErrors { field message } } }`;
+ const d = await gql(q);
+ const ue = d.bulkOperationRunQuery.userErrors;
+ if (ue.length) { console.error('userErrors', ue); process.exit(1); }
+ return d.bulkOperationRunQuery.bulkOperation.id;
+}
+async function poll() {
+ const q = `{ currentBulkOperation(type: QUERY) { id status errorCode objectCount url } }`;
+ for (;;) {
+ const d = await gql(q);
+ const b = d.currentBulkOperation;
+ process.stderr.write(`\r bulk ${b.status} objs=${b.objectCount} `);
+ if (b.status === 'COMPLETED') { process.stderr.write('\n'); return b; }
+ if (['FAILED','CANCELED'].includes(b.status)) { console.error('\nbulk', b); process.exit(1); }
+ await new Promise(r => setTimeout(r, 3000));
+ }
+}
+
+let jsonl;
+if (process.env.USE_CACHE === '1' && fs.existsSync(RAW)) {
+ console.error('using cached raw export', RAW);
+ jsonl = fs.readFileSync(RAW, 'utf8');
+} else {
+ console.error('starting bulk export of all products + global.description_tag ...');
+ await startBulk();
+ const done = await poll();
+ if (!done.url) { console.log('No results url (0 objects).'); process.exit(0); }
+ console.error('downloading', done.objectCount, 'objects ...');
+ jsonl = await (await fetch(done.url)).text();
+ fs.mkdirSync('assets', { recursive: true });
+ fs.writeFileSync(RAW, jsonl);
+ console.error('cached raw ->', RAW);
+}
+
+const lines = jsonl.split('\n').filter(Boolean).map(JSON.parse);
+
+let totalProducts = 0, withTag = 0, inWindow = 0;
+const hits = [];
+const updHist = {};
+for (const n of lines) {
+ if (!n.id || !n.id.includes('/Product/')) continue;
+ totalProducts++;
+ const mf = n.metafield;
+ if (!mf || !mf.updatedAt) continue;
+ withTag++;
+ const day = mf.updatedAt.slice(0,10);
+ updHist[day] = (updHist[day]||0)+1;
+ const t = new Date(mf.updatedAt).getTime();
+ if (t < WIN_LO || t > WIN_HI) continue;
+ inWindow++;
+ hits.push({
+ handle: n.handle, pid: n.id.split('/').pop(), vendor: n.vendor || '',
+ status: n.status, real_title: (n.title||'').trim(),
+ desc_tag: (mf.value||'').trim(), desc_updated_at: mf.updatedAt,
+ });
+}
+
+hits.sort((a,b) => new Date(a.desc_updated_at) - new Date(b.desc_updated_at));
+fs.writeFileSync(`${OUT}-window.json`, JSON.stringify(hits, null, 2));
+
+const byVendor = {}, byStatus = {};
+for (const h of hits) {
+ byVendor[h.vendor||'(none)'] = (byVendor[h.vendor||'(none)']||0)+1;
+ byStatus[h.status] = (byStatus[h.status]||0)+1;
+}
+console.log(JSON.stringify({
+ total_products: totalProducts,
+ with_description_tag: withTag,
+ in_bad_job_window: inWindow,
+ window: ['2026-04-05T16:20:00-07:00','2026-04-05T16:27:00-07:00'],
+ by_status: byStatus,
+ top_vendors: Object.entries(byVendor).sort((a,b)=>b[1]-a[1]).slice(0,25),
+ updatedAt_day_histogram_top: Object.entries(updHist).sort((a,b)=>b[1]-a[1]).slice(0,12),
+ out: `${OUT}-window.json`,
+}, null, 2));
diff --git a/scripts/tk11644-fallback-quality.mjs b/scripts/tk11644-fallback-quality.mjs
new file mode 100644
index 0000000..9cf459a
--- /dev/null
+++ b/scripts/tk11644-fallback-quality.mjs
@@ -0,0 +1,46 @@
+// TK-11644 READ-ONLY fallback-quality canary for the proposed remediation.
+// The fix for a genericized description_tag is to DELETE it so Shopify falls back to a
+// body_html excerpt. That is only a win if body_html is PRESENT and PRODUCT-SPECIFIC.
+// This measures exactly that across the affected ACTIVE population before anything is proposed.
+// No writes. $0.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+const Q=`{ products { edges { node { id handle status descriptionHtml } } } }`;
+const RAW='assets/TK-11644-body-raw.jsonl';
+let jsonl;
+if (fs.existsSync(RAW) && process.env.USE_CACHE==='1') { jsonl=fs.readFileSync(RAW,'utf8'); }
+else {
+ const m=await gql(`mutation { bulkOperationRunQuery(query: ${JSON.stringify(Q)}) { bulkOperation{id status} userErrors{message} } }`);
+ if(m.bulkOperationRunQuery.userErrors.length){console.error(m.bulkOperationRunQuery.userErrors);process.exit(1);}
+ for(;;){ const d=await gql(`{ currentBulkOperation(type:QUERY){ status objectCount url errorCode } }`);
+ const b=d.currentBulkOperation; process.stderr.write(`\r bulk ${b.status} objs=${b.objectCount} `);
+ if(b.status==='COMPLETED'){ process.stderr.write('\n'); jsonl=await (await fetch(b.url)).text(); break; }
+ if(['FAILED','CANCELED'].includes(b.status)){ console.error(b); process.exit(1); }
+ await new Promise(r=>setTimeout(r,3000)); }
+ fs.writeFileSync(RAW,jsonl);
+}
+const body=new Map();
+for(const l of jsonl.split('\n')){ if(!l)continue; const n=JSON.parse(l); if(!n.id?.includes('/Product/'))continue;
+ body.set(n.handle,(n.descriptionHtml||'').replace(/<[^>]+>/g,' ').replace(/&[a-z#0-9]+;/gi,' ').replace(/\s+/g,' ').trim()); }
+const win=JSON.parse(fs.readFileSync('assets/TK-11644-desctag-sweep-window.json','utf8')).filter(r=>r.status==='ACTIVE');
+let empty=0,tooShort=0,good=0,specific=0,missing=0;
+const bad=[];
+for(const r of win){
+ if(!body.has(r.handle)){missing++;continue;}
+ const b=body.get(r.handle);
+ if(!b){empty++; bad.push({handle:r.handle,vendor:r.vendor,why:'empty body'}); continue;}
+ if(b.length<60){tooShort++; bad.push({handle:r.handle,vendor:r.vendor,why:'body <60 chars',body:b}); continue;}
+ good++;
+ const p=r.real_title.split(' | ')[0].split(' - ')[0].trim().toLowerCase();
+ if(p.length>=6 && b.toLowerCase().includes(p)) specific++;
+}
+const n=win.length;
+console.log(JSON.stringify({
+ affected_active: n, body_missing_from_export: missing,
+ body_EMPTY: empty, body_TOO_SHORT_lt60: tooShort,
+ body_USABLE: good, pct_usable: +(100*good/n).toFixed(1),
+ body_NAMES_OWN_PATTERN: specific, pct_specific: +(100*specific/n).toFixed(1),
+ verdict_note: 'delete-to-fallback is only safe for rows with a usable body',
+},null,2));
+fs.writeFileSync('assets/TK-11644-fallback-unsafe.json',JSON.stringify(bad,null,2));
+console.log('unsafe rows ->','assets/TK-11644-fallback-unsafe.json','count',bad.length);
← 7b7ff51 auto-data-snapshot: 2026-09-13T16:30:12 (10 data files) — .g
·
back to Designerwallcoverings
·
TK-11574: fix classifier substring bug + make the job window 7aa2e4e →