← back to Designer Wallcoverings
Surgical scrub of Chesapeake vendor-leak tag from 26 live Shopify products (Versa 'Chesapeake' + Fentucci 'Borders By Chesapeake'); preserve Et Cie 'Chesapeake Bay' colorway
08f38d9855043487973c951fb81e5f426200bc80 · 2026-06-16 13:32:08 -0700 · SteveStudio2
Files touched
A scripts/scrub-chesapeake-tag.js
Diff
commit 08f38d9855043487973c951fb81e5f426200bc80
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue Jun 16 13:32:08 2026 -0700
Surgical scrub of Chesapeake vendor-leak tag from 26 live Shopify products (Versa 'Chesapeake' + Fentucci 'Borders By Chesapeake'); preserve Et Cie 'Chesapeake Bay' colorway
---
scripts/scrub-chesapeake-tag.js | 95 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 95 insertions(+)
diff --git a/scripts/scrub-chesapeake-tag.js b/scripts/scrub-chesapeake-tag.js
new file mode 100644
index 00000000..380cbfc5
--- /dev/null
+++ b/scripts/scrub-chesapeake-tag.js
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+/**
+ * scrub-chesapeake-tag.js — surgical removal of the upstream vendor-leak tag
+ * "Chesapeake" from LIVE Shopify products (Versa Wallcovering line is resold;
+ * "Chesapeake" is the manufacturer and must not leak on the storefront).
+ *
+ * Token-level only: removes EXACTLY the tag(s) whose lowercased value contains
+ * "chesapeake"; never touches any other tag, title, or body. Idempotent.
+ *
+ * Dry-run by default (enumerate + sample). Pass --apply to write.
+ * node scrub-chesapeake-tag.js # dry run
+ * node scrub-chesapeake-tag.js --apply # remove the tag live
+ */
+const https = require('https');
+
+const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
+const SHOPIFY_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const APPLY = process.argv.includes('--apply');
+const NEEDLE = 'chesapeake';
+const DELAY_MS = 350;
+
+if (!SHOPIFY_TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN not set'); process.exit(1); }
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables = {}) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables });
+ const req = https.request({
+ hostname: SHOPIFY_STORE, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+ headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
+ }, res => {
+ let d = ''; res.on('data', c => d += c);
+ res.on('end', () => {
+ try {
+ const p = JSON.parse(d);
+ if (p.errors && JSON.stringify(p.errors).includes('Throttled')) return resolve({ throttled: true });
+ if (p.errors) return reject(new Error(JSON.stringify(p.errors)));
+ resolve(p);
+ } catch (e) { reject(new Error('parse: ' + d.slice(0, 200))); }
+ });
+ });
+ req.on('error', reject); req.write(body); req.end();
+ });
+}
+
+async function gqlRetry(q, v) { for (let i = 0; i < 6; i++) { const r = await gql(q, v); if (r.throttled) { await sleep(2000); continue; } return r; } throw new Error('throttled out'); }
+
+const SEARCH = `query($cursor:String){ products(first:100, after:$cursor, query:"tag:Chesapeake"){ pageInfo{hasNextPage endCursor} edges{ node{ id title status tags vendor } } } }`;
+const MUT = `mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id, tags:$tags){ userErrors{field message} } }`;
+
+// SURGICAL leak rule (NOT a blind substring): "Chesapeake Bay" is a legitimate
+// colorway/place name on Et Cie designer murals — KEEP. Only these are leaks:
+// • exact tag "Chesapeake" on vendor "Versa Designed Surfaces" (upstream mfr leak)
+// • exact tag "Borders By Chesapeake" (any vendor — explicitly names the mfr)
+function leakTags(node) {
+ const out = [];
+ for (const t of (node.tags || [])) {
+ const tl = t.trim().toLowerCase();
+ if (tl === 'borders by chesapeake') out.push(t);
+ else if (tl === 'chesapeake' && node.vendor === 'Versa Designed Surfaces') out.push(t);
+ }
+ return out;
+}
+
+(async () => {
+ console.log(`scrub-chesapeake-tag — ${APPLY ? 'APPLY (live writes)' : 'DRY RUN'} — needle="${NEEDLE}"`);
+ let cursor = null, hasNext = true, all = [];
+ while (hasNext) {
+ const r = await gqlRetry(SEARCH, { cursor });
+ const conn = r.data.products;
+ for (const e of conn.edges) {
+ const hit = leakTags(e.node);
+ if (hit.length) all.push({ id: e.node.id, title: e.node.title, status: e.node.status, vendor: e.node.vendor, hitTags: hit });
+ }
+ hasNext = conn.pageInfo.hasNextPage; cursor = conn.pageInfo.endCursor;
+ }
+ console.log(`Found ${all.length} products carrying a Chesapeake tag.`);
+ const byStatus = all.reduce((a, p) => { a[p.status] = (a[p.status] || 0) + 1; return a; }, {});
+ console.log(' by status:', JSON.stringify(byStatus));
+ console.log(' sample:', all.slice(0, 8).map(p => `${p.title} [${p.hitTags.join('|')}]`).join('\n '));
+
+ if (!APPLY) { console.log('\nDRY RUN — no writes. Re-run with --apply to remove.'); return; }
+
+ let ok = 0, fail = 0;
+ for (const p of all) {
+ try {
+ const r = await gqlRetry(MUT, { id: p.id, tags: p.hitTags });
+ const ue = r.data.tagsRemove.userErrors;
+ if (ue && ue.length) { fail++; console.log(` ✗ ${p.title}: ${JSON.stringify(ue)}`); }
+ else { ok++; if (ok % 25 === 0) console.log(` …${ok}/${all.length}`); }
+ } catch (e) { fail++; console.log(` ✗ ${p.title}: ${e.message}`); }
+ await sleep(DELAY_MS);
+ }
+ console.log(`\nDONE — removed Chesapeake tag from ${ok} products, ${fail} failed.`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← 144c9df6 China Seas de-header: COMPLETE — 1810 products pushed (band
·
back to Designer Wallcoverings
·
gitignore scraper output (vendor-scrapers 4.5G) + large data 16b2e388 →