← back to Designerwallcoverings
TK-11574: exhaustive phrasing-agnostic title_tag backstop (read-only bulk export + identity classifier)
230e03d39357112bf9f0192139b0830848dfc99c · 2026-09-13 16:24:13 -0700 · Steve Abrams
Full-catalog global.title_tag {value,updatedAt} bulk export plus an identity-based
classifier, replacing TK-11564's three text-signature matches.
- tk11574-titletag-backstop.mjs refuses to download a bulk operation it did not start
(PROVENANCE FAIL). It previously attached to a concurrent session's op that was
exporting global.description_tag, which would have reported the wrong field.
- tk11574_classify.py / tk11574_subclass2.py: product-identity test + defect tiers.
Read-only, no Shopify writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eu4qMc5WBStSPq2FSnEcAG
Files touched
A scripts/tk11574-titletag-backstop.mjsA scripts/tk11574_classify.pyA scripts/tk11574_subclass2.py
Diff
commit 230e03d39357112bf9f0192139b0830848dfc99c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:24:13 2026 -0700
TK-11574: exhaustive phrasing-agnostic title_tag backstop (read-only bulk export + identity classifier)
Full-catalog global.title_tag {value,updatedAt} bulk export plus an identity-based
classifier, replacing TK-11564's three text-signature matches.
- tk11574-titletag-backstop.mjs refuses to download a bulk operation it did not start
(PROVENANCE FAIL). It previously attached to a concurrent session's op that was
exporting global.description_tag, which would have reported the wrong field.
- tk11574_classify.py / tk11574_subclass2.py: product-identity test + defect tiers.
Read-only, no Shopify writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eu4qMc5WBStSPq2FSnEcAG
---
scripts/tk11574-titletag-backstop.mjs | 128 ++++++++++++++++++++++++++++++++++
scripts/tk11574_classify.py | 79 +++++++++++++++++++++
scripts/tk11574_subclass2.py | 57 +++++++++++++++
3 files changed, 264 insertions(+)
diff --git a/scripts/tk11574-titletag-backstop.mjs b/scripts/tk11574-titletag-backstop.mjs
new file mode 100644
index 0000000..346e93e
--- /dev/null
+++ b/scripts/tk11574-titletag-backstop.mjs
@@ -0,0 +1,128 @@
+// TK-11574 — EXHAUSTIVE read-only backstop for the 2026-04-05 16:20-16:26 PT bad bulk
+// SEO-title job. TK-11564/TK-11558 remediated by TEXT SIGNATURE (3 known phrasings);
+// this pass is PHRASING-AGNOSTIC: bulk-export EVERY product's global.title_tag
+// {value, updatedAt}, keep every row whose metafield was written inside the job window,
+// and emit the complete set for identity-based classification.
+//
+// No writes. Pure GraphQL bulk read. $0.
+import { gql } from './lib/shopify.mjs';
+import fs from 'node:fs';
+
+// Window per ticket: 2026-04-05T23:20:00Z .. 23:26:59Z (= 16:20-16:26 PT)
+// Padded by 60s on each side so a boundary write cannot escape the backstop.
+const WIN_LO = Date.parse('2026-04-05T23:19:00Z');
+const WIN_HI = Date.parse('2026-04-05T23:27:59Z');
+const OUT = process.env.TK11574_OUT || '/tmp/tk11574_titletag_backstop.jsonl';
+const META = OUT.replace(/\.jsonl$/, '.meta.json');
+const RAW = process.env.TK11574_RAW || '/tmp/tk11574_bulk_raw.jsonl';
+
+const BULK_QUERY = `{
+ products {
+ edges { node {
+ id handle vendor title status productType tags
+ metafield(namespace:"global", key:"title_tag") { value updatedAt }
+ } }
+ }
+}`;
+
+async function currentOp() {
+ const d = await gql(`{ currentBulkOperation(type: QUERY) { id status errorCode objectCount url createdAt } }`);
+ return d.currentBulkOperation;
+}
+async function startBulk() {
+ // NEVER attach to a bulk op this script did not start. Another session's op can carry a
+ // DIFFERENT query (this bit us once: a concurrent global.description_tag export was
+ // mislabelled as title_tag). Wait for the app's op slot to free, then start OUR op and
+ // return ITS id so poll() can assert provenance.
+ for (;;) {
+ const cur = await currentOp();
+ if (!cur || !['CREATED', 'RUNNING'].includes(cur.status)) break;
+ process.stderr.write(`\r waiting for foreign bulk op ${cur.id} (${cur.status}, objs=${cur.objectCount}) to finish... `);
+ await new Promise(r => setTimeout(r, 5000));
+ }
+ process.stderr.write('\n');
+ const q = `mutation { bulkOperationRunQuery(query: ${JSON.stringify(BULK_QUERY)}) {
+ bulkOperation { id status } userErrors { field message } } }`;
+ const d = await gql(q);
+ if (d.__err) { console.error('gql error', JSON.stringify(d.__err)); process.exit(1); }
+ const ue = d.bulkOperationRunQuery.userErrors;
+ if (ue.length) { console.error('userErrors', ue); process.exit(1); }
+ return d.bulkOperationRunQuery.bulkOperation.id;
+}
+async function poll(myId) {
+ for (;;) {
+ const b = await currentOp();
+ if (b && myId && b.id !== myId) { console.error(`\nPROVENANCE FAIL: currentBulkOperation is ${b.id}, expected ${myId}. Refusing to download a foreign op's results.`); process.exit(1); }
+ process.stderr.write(`\r bulk ${b?.status} objs=${b?.objectCount} `);
+ if (b?.status === 'COMPLETED') { process.stderr.write('\n'); return b; }
+ if (['FAILED', 'CANCELED', 'EXPIRED'].includes(b?.status)) { console.error('\nbulk failed', b); process.exit(1); }
+ await new Promise(r => setTimeout(r, 4000));
+ }
+}
+
+const t0 = Date.now();
+console.error('TK-11574 backstop: starting full-catalog bulk export (products + global.title_tag)...');
+const myId = await startBulk();
+const done = await poll(myId);
+if (!done.url) { console.error('no results url (0 objects)'); process.exit(1); }
+
+console.error(`downloading ${done.objectCount} objects ...`);
+const res = await fetch(done.url);
+const jsonl = await res.text();
+fs.writeFileSync(RAW, jsonl);
+const lines = jsonl.split('\n').filter(Boolean);
+
+let totalProducts = 0, withTag = 0, inWindow = 0, noTag = 0;
+let tsMin = null, tsMax = null;
+const byVendor = {}, byStatus = {};
+const rows = [];
+const outFh = fs.openSync(OUT, 'w');
+
+for (const raw of lines) {
+ let n; try { n = JSON.parse(raw); } catch { continue; }
+ if (!n.id || !String(n.id).includes('/Product/')) continue;
+ totalProducts++;
+ const mf = n.metafield;
+ if (!mf || !mf.updatedAt) { noTag++; continue; }
+ withTag++;
+ const t = Date.parse(mf.updatedAt);
+ if (!(t >= WIN_LO && t <= WIN_HI)) continue;
+ inWindow++;
+ if (!tsMin || mf.updatedAt < tsMin) tsMin = mf.updatedAt;
+ if (!tsMax || mf.updatedAt > tsMax) tsMax = mf.updatedAt;
+ byVendor[n.vendor || '(none)'] = (byVendor[n.vendor || '(none)'] || 0) + 1;
+ byStatus[n.status] = (byStatus[n.status] || 0) + 1;
+ const row = {
+ pid: String(n.id).split('/').pop(),
+ handle: n.handle,
+ vendor: n.vendor || '',
+ status: n.status,
+ product_type: n.productType || '',
+ real_title: (n.title || '').trim(),
+ title_tag: (mf.value || '').trim(),
+ updated_at: mf.updatedAt,
+ };
+ rows.push(row);
+ fs.writeSync(outFh, JSON.stringify(row) + '\n');
+}
+fs.closeSync(outFh);
+
+const meta = {
+ ticket: 'TK-11574',
+ generated_at: new Date().toISOString(),
+ elapsed_sec: Math.round((Date.now() - t0) / 1000),
+ bulk_object_count: done.objectCount,
+ window_utc: ['2026-04-05T23:19:00Z', '2026-04-05T23:27:59Z'],
+ total_products: totalProducts,
+ products_with_title_tag: withTag,
+ products_without_title_tag: noTag,
+ in_window: inWindow,
+ ts_min: tsMin, ts_max: tsMax,
+ by_status: byStatus,
+ by_vendor_top: Object.fromEntries(Object.entries(byVendor).sort((a, b) => b[1] - a[1]).slice(0, 40)),
+ vendor_count: Object.keys(byVendor).length,
+ out: OUT, raw: RAW,
+};
+fs.writeFileSync(META, JSON.stringify(meta, null, 1));
+console.error(JSON.stringify(meta, null, 1));
+console.error(`\nrows -> ${OUT}\nmeta -> ${META}\nraw -> ${RAW}`);
diff --git a/scripts/tk11574_classify.py b/scripts/tk11574_classify.py
new file mode 100644
index 0000000..eb13cb2
--- /dev/null
+++ b/scripts/tk11574_classify.py
@@ -0,0 +1,79 @@
+import json, collections, re, sys
+
+rows = json.load(open('tk11574_window_rows.json'))
+
+GENERIC = set("""wallcovering wallcoverings wallpaper wallpapers fabric fabrics sample samples mural murals
+commercial residential print prints panel panels new type collection collections designer designerwallcoverings
+color colour vinyl textile textiles grasscloth pillow pillows trim trims border borders roll rolls yard yards
+the and for with from your our all one stop shop now available purchasing premium luxury luxurious
+art wall walls europe usa inc llc co ltd studio design designs series style styles""".split())
+
+def norm(s):
+ return re.sub(r'[^a-z0-9 ]+', ' ', (s or '').lower())
+
+def toks(s):
+ return [t for t in norm(s).split() if t]
+
+def distinctive(title, vendor):
+ vt = set(toks(vendor))
+ out = []
+ for t in toks(title):
+ if len(t) < 4: continue
+ if t in GENERIC: continue
+ if t in vt: continue
+ out.append(t)
+ return out
+
+res = []
+for r in rows:
+ d = distinctive(r['title'], r['vendor'])
+ tagn = ' ' + norm(r['tag']) + ' '
+ if not d:
+ verdict = 'NOT_MEASURED' # no distinctive token to test against
+ hit = []
+ else:
+ hit = [t for t in d if (' ' + t + ' ') in tagn or t in tagn]
+ verdict = 'HAS_IDENTITY' if hit else 'NO_IDENTITY'
+ r2 = dict(r)
+ r2['distinctive'] = d
+ r2['identity_hits'] = hit
+ r2['identity'] = verdict
+ r2['len'] = len(r['tag'])
+ res.append(r2)
+
+json.dump(res, open('tk11574_classified.json','w'))
+
+def tally(sel, label):
+ c = collections.Counter(x['identity'] for x in sel)
+ ln = collections.Counter('len>60' if x['len']>60 else 'len<=60' for x in sel)
+ print(f'{label}: n={len(sel)} identity={dict(c)} {dict(ln)}')
+
+tally(res, 'ALL in-window')
+for st in ('ACTIVE','ARCHIVED','DRAFT'):
+ tally([x for x in res if x['status']==st], f' {st}')
+
+act = [x for x in res if x['status']=='ACTIVE']
+print()
+print('ACTIVE breakdown:')
+no_id = [x for x in act if x['identity']=='NO_IDENTITY']
+has = [x for x in act if x['identity']=='HAS_IDENTITY']
+nm = [x for x in act if x['identity']=='NOT_MEASURED']
+print(f' NO_IDENTITY (does not describe its product): {len(no_id)}')
+print(f' HAS_IDENTITY: {len(has)} of which len>60: {sum(1 for x in has if x["len"]>60)}')
+print(f' NOT_MEASURED (no distinctive token in real title): {len(nm)}')
+print()
+print('ACTIVE NO_IDENTITY by vendor (top 30):')
+for v,n in collections.Counter(x['vendor'] or '(none)' for x in no_id).most_common(30):
+ print(f' {n:7d} {v}')
+print()
+print('ACTIVE NO_IDENTITY sample rows:')
+for x in no_id[:12]:
+ print(f' [{x["vendor"]}] {x["title"][:60]!r}\n TAG: {x["tag"][:120]!r}')
+print()
+print('ACTIVE HAS_IDENTITY examples (len<=60):')
+for x in [y for y in has if y['len']<=60][:8]:
+ print(f' [{x["vendor"]}] {x["title"][:60]!r}\n TAG: {x["tag"][:120]!r}')
+print()
+print('ACTIVE NOT_MEASURED examples:')
+for x in nm[:8]:
+ print(f' [{x["vendor"]}] {x["title"][:60]!r} TAG: {x["tag"][:90]!r}')
diff --git a/scripts/tk11574_subclass2.py b/scripts/tk11574_subclass2.py
new file mode 100644
index 0000000..bf453e1
--- /dev/null
+++ b/scripts/tk11574_subclass2.py
@@ -0,0 +1,57 @@
+import json, collections, re
+rows = json.load(open('tk11574_classified.json'))
+def norm(s): return re.sub(r'[^a-z0-9 ]+',' ',(s or '').lower())
+def sq(s): return ' '.join(norm(s).split())
+
+TRUNC = re.compile(r'(\.\.\.|…)\s*$')
+
+out=[]
+for r in rows:
+ tag=r['tag']; title=r['title']; vendor=r['vendor']
+ tn, ttl = sq(tag), sq(title)
+ flags=[]
+ if r['identity']=='NO_IDENTITY': flags.append('no_product_identity')
+ if r['identity']=='NOT_MEASURED': flags.append('not_measured')
+ if TRUNC.search(tag): flags.append('truncated_ellipsis')
+ # doubled VENDOR suffix: vendor phrase occurs >=2x in tag but <=1x in the real title
+ v=sq(vendor)
+ if v and len(v)>4:
+ ct, cl = tn.count(v), ttl.count(v)
+ if ct>=2 and ct>cl: flags.append('doubled_vendor')
+ # doubled arbitrary phrase: 3+ word phrase repeated in tag but NOT repeated in the real title
+ w=tn.split(); seen={}
+ for n in (3,4):
+ for i in range(len(w)-n+1):
+ p=' '.join(w[i:i+n])
+ if p in seen and ttl.count(p)<2:
+ flags.append('doubled_phrase'); break
+ seen[p]=i
+ if 'doubled_phrase' in flags: break
+ if len(tag)>60: flags.append('over_60_chars')
+ if len(tag)<10: flags.append('under_10_chars')
+ if tn==ttl: flags.append('identical_to_title')
+ r2=dict(r); r2['flags']=flags; out.append(r2)
+json.dump(out, open('tk11574_flagged.json','w'))
+
+act=[x for x in out if x['status']=='ACTIVE']
+def n(sel,f): return sum(1 for x in sel if f in x['flags'])
+print(f'in-window total {len(out)} ACTIVE {len(act)} ARCHIVED {sum(1 for x in out if x["status"]=="ARCHIVED")} DRAFT {sum(1 for x in out if x["status"]=="DRAFT")}')
+print()
+hdr=f'{"defect":26s}{"ACTIVE":>9s}{"ALL":>9s}'
+print(hdr); print('-'*len(hdr))
+for f in ('no_product_identity','truncated_ellipsis','doubled_vendor','doubled_phrase','not_measured','under_10_chars','over_60_chars','identical_to_title'):
+ print(f'{f:26s}{n(act,f):>9d}{n(out,f):>9d}')
+print()
+clean=[x for x in act if not (set(x['flags'])-{'over_60_chars','identical_to_title'})]
+print(f'ACTIVE with NO substantive defect (length/identical only): {len(clean)}')
+defect=[x for x in act if (set(x['flags'])-{'over_60_chars','identical_to_title'})]
+print(f'ACTIVE with >=1 substantive defect: {len(defect)}')
+print()
+print('--- doubled_vendor ACTIVE examples')
+for x in [y for y in act if 'doubled_vendor' in y['flags']][:6]:
+ print(f' [{x["vendor"]}] {x["title"][:50]!r}\n TAG {x["tag"][:95]!r}')
+print('--- doubled_phrase ACTIVE examples')
+for x in [y for y in act if 'doubled_phrase' in y['flags'] and 'doubled_vendor' not in y['flags']][:6]:
+ print(f' [{x["vendor"]}] {x["title"][:50]!r}\n TAG {x["tag"][:95]!r}')
+json.dump(defect, open('tk11574_active_defects.json','w'))
+print(f'\nwrote {len(defect)} ACTIVE defect rows -> tk11574_active_defects.json')
← 628279c TK-11635: restore rollback fidelity — recreate weight + trac
·
back to Designerwallcoverings
·
TK-11635: re-check each variant immediately before its delet 5f814e0 →