[object Object]

← back to Designerwallcoverings

TK-11564: read-only title_tag catalog-corruption blast-radius sweep

bfbd468da72491bb0952a932caaa9290210b4ebf · 2026-09-13 00:40:24 -0700 · Steve Abrams

Bulk-exports every product + global.title_tag (value+updatedAt), filters to the
2026-04-05 16:20-16:26 PT bad-SEO-job window, subclassifies corruption (cross-copy
wrong product / generic no-identity / template-prefix / promo suffix). No writes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit bfbd468da72491bb0952a932caaa9290210b4ebf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Sep 13 00:40:24 2026 -0700

    TK-11564: read-only title_tag catalog-corruption blast-radius sweep
    
    Bulk-exports every product + global.title_tag (value+updatedAt), filters to the
    2026-04-05 16:20-16:26 PT bad-SEO-job window, subclassifies corruption (cross-copy
    wrong product / generic no-identity / template-prefix / promo suffix). No writes.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/tk11564-titletag-sweep.mjs | 101 +++++++++++++++++++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/scripts/tk11564-titletag-sweep.mjs b/scripts/tk11564-titletag-sweep.mjs
new file mode 100644
index 0000000..02fbbcf
--- /dev/null
+++ b/scripts/tk11564-titletag-sweep.mjs
@@ -0,0 +1,101 @@
+// TK-11564 READ-ONLY blast-radius sweep: find every product whose global.title_tag
+// metafield was written by the 2026-04-05 16:20-16:26 PT bad bulk SEO job (the same
+// job that corrupted the 25 products in TK-11558). Bulk-export all products + their
+// title_tag metafield (value+updatedAt), filter to the window, flag corruption sigs.
+// No writes. $0 read-only.
+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 BULK_QUERY = `{
+  products {
+    edges { node {
+      id handle vendor title status
+      metafield(namespace:"global", key:"title_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));
+  }
+}
+
+console.error('starting bulk export of all products + global.title_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 ...');
+const jsonl = await (await fetch(done.url)).text();
+const lines = jsonl.split('\n').filter(Boolean).map(JSON.parse);
+
+// Bulk flattens: product nodes have gid/Product; metafield is inline on the product node here
+// (single field, not a connection) so each product line carries .metafield when present.
+let totalProducts = 0, withTag = 0;
+const hits = [];
+const SIG = [
+  /^Free Samples & Purchasing Available for/i,
+  /Samples and Purchasing at Designer Wallcoverings/i,
+  /Authorized Dealer of .* Samples and Purchasing/i,
+];
+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 t = new Date(mf.updatedAt).getTime();
+  if (t < WIN_LO || t > WIN_HI) continue;
+  const realTitle = (n.title || '').trim();
+  const seo = (mf.value || '').trim();
+  const mismatch = seo && realTitle && seo.toLowerCase() !== realTitle.toLowerCase()
+    && !seo.toLowerCase().startsWith(realTitle.toLowerCase());
+  hits.push({
+    handle: n.handle, pid: n.id.split('/').pop(), vendor: n.vendor,
+    status: n.status, real_title: realTitle, seo_title_tag: seo,
+    seo_updated_at: mf.updatedAt, mismatch,
+    signature: SIG.some(r => r.test(seo)),
+  });
+}
+
+hits.sort((a,b) => new Date(a.seo_updated_at) - new Date(b.seo_updated_at));
+const byVendor = {}; for (const h of hits) byVendor[h.vendor||'(none)'] = (byVendor[h.vendor||'(none)']||0)+1;
+const active = hits.filter(h=>h.status==='ACTIVE').length;
+const mism = hits.filter(h=>h.mismatch).length;
+const sig = hits.filter(h=>h.signature).length;
+const tsMin = hits.length ? hits[0].seo_updated_at : null;
+const tsMax = hits.length ? hits[hits.length-1].seo_updated_at : null;
+
+const out = { generated_at: new Date().toISOString(), total_products: totalProducts,
+  products_with_title_tag: withTag, window: '2026-04-05T16:20:00..16:27:00-07:00',
+  hits_in_window: hits.length, active, mismatch: mism, signature_title: sig,
+  ts_min: tsMin, ts_max: tsMax, by_vendor: byVendor, rows: hits };
+const path = '/tmp/tk11564_titletag_sweep.json';
+fs.writeFileSync(path, JSON.stringify(out, null, 1));
+
+console.log(`\nTotal products scanned:        ${totalProducts}`);
+console.log(`Have global.title_tag:         ${withTag}`);
+console.log(`Written in bad-job window:     ${hits.length}   (${tsMin} .. ${tsMax})`);
+console.log(`  of which ACTIVE:             ${active}`);
+console.log(`  title != real title:        ${mism}`);
+console.log(`  carry known artifact sig:    ${sig}`);
+console.log('By vendor:', JSON.stringify(byVendor));
+console.log('Full snapshot ->', path);

← 669e900 Add run-app project skill: verified launch recipe for the ro  ·  back to Designerwallcoverings  ·  TK-11465: executed Hollywood pattern_name backfill — 234/234 667be31 →