← 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
A scripts/tk11564-titletag-sweep.mjs
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 →